Skip to content

Commit 297b3cc

Browse files
committed
Make calendar examples more consistent. Fix README files. Add tests to declarative version.
1 parent c1c51da commit 297b3cc

File tree

16 files changed

+498
-69
lines changed

16 files changed

+498
-69
lines changed

examples/calendar-application/README.md renamed to examples/calendar-application/calendar-declarative/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ When the MCP Inspector starts, it will automatically open a new browser window.
2727
MCP server:
2828

2929
1. On the left panel of the Inspector UI, configure the connection settings.
30-
2. Set the **Transport** to `SSE`.
30+
2. Set the **Transport** to `SSE` or `Stremeable HTTP`.
3131
3. Update the **URL** field to: `http://localhost:8081/calendar`
3232
4. Click the **Connect** button to establish a connection to the server.
3333

3434
### Testing the Tool
3535

36-
1. Navigate to the **Tool** tab.
36+
1. Navigate to the **Tools** tab.
3737
2. Click **List Tools** and select the `addCalendarEventTool` tool from the list.
3838
3. Enter the following parameters on the right panel:
3939

@@ -45,14 +45,14 @@ MCP server:
4545

4646
### Testing the Resource
4747

48-
1. Navigate to the **Resource** tab.
48+
1. Navigate to the **Resources** tab.
4949
2. Click **List Resources**.
5050
3. Select the first resource in the list.
5151
4. Verify that the result displayed includes Frank's birthday event.
5252

5353
### Testing the Resource Template
5454

55-
1. Navigate to the **Resource** tab.
55+
1. Navigate to the **Resources** tab.
5656
2. Click **List Resources Template**.
5757
3. Select `eventResourceTemplate`.
5858
3. Type `F` for the name to trigger completion.

examples/calendar-application/calendar-declarative/src/main/java/io/helidon/extensions/mcp/examples/calendar/declarative/McpCalendarServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class McpCalendarServer {
6262
* @return list of calendar events
6363
*/
6464
@Mcp.Tool("List calendar events")
65-
List<McpToolContent> listCalendarEvent(String date) {
65+
List<McpToolContent> listCalendarEvents(String date) {
6666
String entries = calendar.readContentMatchesLine(
6767
line -> date.isEmpty() || line.contains("date: " + date));
6868
return List.of(McpToolContents.textContent(entries));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.extensions.mcp.examples.calendar.declarative;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import io.helidon.common.media.type.MediaTypes;
23+
24+
import io.modelcontextprotocol.client.McpSyncClient;
25+
import io.modelcontextprotocol.spec.McpSchema;
26+
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
27+
import org.junit.jupiter.api.Order;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.TestMethodOrder;
30+
31+
import static org.hamcrest.MatcherAssert.assertThat;
32+
import static org.hamcrest.Matchers.containsString;
33+
import static org.hamcrest.Matchers.hasItems;
34+
import static org.hamcrest.Matchers.instanceOf;
35+
import static org.hamcrest.Matchers.is;
36+
import static org.hamcrest.Matchers.startsWith;
37+
38+
@TestMethodOrder(OrderAnnotation.class)
39+
abstract class BaseTest {
40+
41+
abstract McpSyncClient client();
42+
43+
@Test
44+
@Order(1)
45+
void testToolList() {
46+
McpSchema.ListToolsResult listTool = client().listTools();
47+
List<McpSchema.Tool> tools = listTool.tools();
48+
assertThat(tools.size(), is(2));
49+
50+
McpSchema.Tool tool1 = tools.getFirst();
51+
assertThat(tool1.name(), is("listCalendarEvents"));
52+
assertThat(tool1.description(), is("List calendar events"));
53+
54+
McpSchema.JsonSchema schema1 = tool1.inputSchema();
55+
assertThat(schema1.type(), is("object"));
56+
assertThat(schema1.properties().keySet(), hasItems("date"));
57+
58+
McpSchema.Tool tool2 = tools.getLast();
59+
assertThat(tool2.name(), is("addCalendarEvent"));
60+
assertThat(tool2.description(), is("Adds a new event to the calendar"));
61+
62+
McpSchema.JsonSchema schema2 = tool2.inputSchema();
63+
assertThat(schema2.type(), is("object"));
64+
assertThat(schema2.properties().keySet(), hasItems("name", "date", "attendees"));
65+
}
66+
67+
@Test
68+
@Order(2)
69+
void testAddToolCall() {
70+
Map<String, Object> arguments = Map.of("name", "Frank-birthday",
71+
"date", "2021-04-20",
72+
"attendees", List.of("Frank"));
73+
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("addCalendarEvent", arguments);
74+
McpSchema.CallToolResult result = client().callTool(request);
75+
assertThat(result.isError(), is(false));
76+
77+
List<McpSchema.Content> contents = result.content();
78+
assertThat(contents.size(), is(1));
79+
80+
McpSchema.Content content = contents.getFirst();
81+
assertThat(content.type(), is("text"));
82+
assertThat(content, instanceOf(McpSchema.TextContent.class));
83+
84+
McpSchema.TextContent textContent = (McpSchema.TextContent) content;
85+
assertThat(textContent.text(), is("New event added to the calendar"));
86+
}
87+
88+
@Test
89+
@Order(3)
90+
void testListToolCall() {
91+
Map<String, Object> arguments = Map.of("date", "2021-04-20");
92+
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("listCalendarEvents", arguments);
93+
McpSchema.CallToolResult result = client().callTool(request);
94+
assertThat(result.isError(), is(false));
95+
96+
List<McpSchema.Content> contents = result.content();
97+
assertThat(contents.size(), is(1));
98+
99+
McpSchema.Content content = contents.getFirst();
100+
assertThat(content.type(), is("text"));
101+
assertThat(content, instanceOf(McpSchema.TextContent.class));
102+
103+
McpSchema.TextContent textContent = (McpSchema.TextContent) content;
104+
assertThat(textContent.text(), containsString("Frank-birthday"));
105+
}
106+
107+
@Test
108+
@Order(4)
109+
void testPromptList() {
110+
McpSchema.ListPromptsResult listPrompt = client().listPrompts();
111+
List<McpSchema.Prompt> prompts = listPrompt.prompts();
112+
assertThat(prompts.size(), is(1));
113+
114+
McpSchema.Prompt prompt = prompts.getFirst();
115+
assertThat(prompt.name(), is("createEventPrompt"));
116+
assertThat(prompt.description(), is("Prompt to create a new event given a name, date and attendees"));
117+
118+
List<McpSchema.PromptArgument> arguments = prompt.arguments();
119+
arguments.sort(this::sortArguments);
120+
assertThat(arguments.size(), is(3));
121+
122+
McpSchema.PromptArgument attendees = arguments.getFirst();
123+
assertThat(attendees.name(), is("attendees"));
124+
assertThat(attendees.description(), is("event's attendees"));
125+
assertThat(attendees.required(), is(true));
126+
127+
McpSchema.PromptArgument date = arguments.get(1);
128+
assertThat(date.name(), is("date"));
129+
assertThat(date.description(), is("event's date"));
130+
assertThat(date.required(), is(true));
131+
132+
McpSchema.PromptArgument name = arguments.getLast();
133+
assertThat(name.name(), is("name"));
134+
assertThat(name.description(), is("event's name"));
135+
assertThat(name.required(), is(true));
136+
}
137+
138+
@Test
139+
@Order(5)
140+
void testPromptCall() {
141+
Map<String, Object> arguments = Map.of("name", "Frank-birthday", "date", "2021-04-20", "attendees", "Frank");
142+
McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("createEventPrompt", arguments);
143+
McpSchema.GetPromptResult promptResult = client().getPrompt(request);
144+
assertThat(promptResult.description(), is("Prompt to create a new event given a name, date and attendees"));
145+
146+
List<McpSchema.PromptMessage> messages = promptResult.messages();
147+
assertThat(messages.size(), is(1));
148+
149+
McpSchema.PromptMessage message = messages.getFirst();
150+
assertThat(message.role(), is(McpSchema.Role.USER));
151+
assertThat(message.content(), instanceOf(McpSchema.TextContent.class));
152+
153+
McpSchema.TextContent textContent = (McpSchema.TextContent) message.content();
154+
assertThat(textContent.text(), is(
155+
"""
156+
Create a new calendar event with name Frank-birthday, on 2021-04-20 with attendees Frank. Make
157+
sure all attendees are free to attend the event.
158+
"""));
159+
}
160+
161+
@Test
162+
@Order(6)
163+
void testResourceList() {
164+
McpSchema.ListResourcesResult result = client().listResources();
165+
List<McpSchema.Resource> resources = result.resources();
166+
assertThat(resources.size(), is(1));
167+
168+
McpSchema.Resource resource = resources.getFirst();
169+
assertThat(resource.name(), is("eventsResource"));
170+
assertThat(resource.uri(), startsWith("file://"));
171+
assertThat(resource.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE));
172+
assertThat(resource.description(), is("List of calendar events created"));
173+
}
174+
175+
@Test
176+
@Order(7)
177+
void testResourceCall() {
178+
String uri = client().listResources().resources().getFirst().uri();
179+
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest(uri);
180+
McpSchema.ReadResourceResult result = client().readResource(request);
181+
182+
List<McpSchema.ResourceContents> contents = result.contents();
183+
assertThat(contents.size(), is(1));
184+
185+
McpSchema.ResourceContents content = contents.getFirst();
186+
assertThat(content.uri(), is(uri));
187+
assertThat(content.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE));
188+
assertThat(content, instanceOf(McpSchema.TextResourceContents.class));
189+
190+
McpSchema.TextResourceContents textContent = (McpSchema.TextResourceContents) content;
191+
assertThat(textContent.text(), is("Event: { name: Frank-birthday, date: 2021-04-20, attendees: [Frank] }\n"));
192+
}
193+
194+
@Test
195+
@Order(8)
196+
void testResourceTemplateList() {
197+
McpSchema.ListResourceTemplatesResult result = client().listResourceTemplates();
198+
List<McpSchema.ResourceTemplate> templates = result.resourceTemplates();
199+
assertThat(templates.size(), is(1));
200+
201+
McpSchema.ResourceTemplate template = templates.getFirst();
202+
assertThat(template.uriTemplate(), containsString("{name}"));
203+
assertThat(template.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE));
204+
assertThat(template.name(), is("eventResourceTemplate"));
205+
assertThat(template.description(), is("List single calendar event by name"));
206+
}
207+
208+
@Test
209+
@Order(9)
210+
void testResourceTemplateCall() {
211+
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("file://events/Frank-birthday");
212+
McpSchema.ReadResourceResult result = client().readResource(request);
213+
var contents = result.contents();
214+
assertThat(contents.size(), is(1));
215+
216+
McpSchema.ResourceContents content = contents.getFirst();
217+
assertThat(content, instanceOf(McpSchema.TextResourceContents.class));
218+
219+
McpSchema.TextResourceContents text = (McpSchema.TextResourceContents) content;
220+
assertThat(content.uri(), is("file://events/Frank-birthday"));
221+
assertThat(content.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE));
222+
assertThat(text.text(), is("Event: { name: Frank-birthday, date: 2021-04-20, attendees: [Frank] }"));
223+
}
224+
225+
@Test
226+
@Order(10)
227+
void testCalendarEventPromptCompletion() {
228+
McpSchema.CompleteRequest request1 = new McpSchema.CompleteRequest(
229+
new McpSchema.PromptReference("createEventPrompt"),
230+
new McpSchema.CompleteRequest.CompleteArgument("name", ""));
231+
McpSchema.CompleteResult.CompleteCompletion completion1 = client().completeCompletion(request1).completion();
232+
assertThat(completion1.hasMore(), is(false));
233+
assertThat(completion1.total(), is(1));
234+
assertThat(completion1.values().size(), is(1));
235+
assertThat(completion1.values().getFirst(), is("Frank & Friends"));
236+
237+
McpSchema.CompleteRequest request2 = new McpSchema.CompleteRequest(
238+
new McpSchema.PromptReference("createEventPrompt"),
239+
new McpSchema.CompleteRequest.CompleteArgument("date", ""));
240+
McpSchema.CompleteResult.CompleteCompletion completion2 = client().completeCompletion(request2).completion();
241+
assertThat(completion2.hasMore(), is(false));
242+
assertThat(completion2.total(), is(3));
243+
assertThat(completion2.values().size(), is(3));
244+
245+
McpSchema.CompleteRequest request3 = new McpSchema.CompleteRequest(
246+
new McpSchema.PromptReference("createEventPrompt"),
247+
new McpSchema.CompleteRequest.CompleteArgument("attendees", ""));
248+
McpSchema.CompleteResult.CompleteCompletion completion3 = client().completeCompletion(request3).completion();
249+
assertThat(completion3.hasMore(), is(false));
250+
assertThat(completion3.total(), is(3));
251+
assertThat(completion3.values().size(), is(3));
252+
}
253+
254+
@Test
255+
@Order(11)
256+
void testCalendarEventResourceCompletion() {
257+
McpSchema.CompleteRequest request = new McpSchema.CompleteRequest(
258+
new McpSchema.ResourceReference(McpCalendarServer.EVENTS_URI_TEMPLATE),
259+
new McpSchema.CompleteRequest.CompleteArgument("name", ""));
260+
McpSchema.CompleteResult.CompleteCompletion completion = client().completeCompletion(request).completion();
261+
assertThat(completion.hasMore(), is(false));
262+
assertThat(completion.total(), is(1));
263+
assertThat(completion.values().size(), is(1));
264+
assertThat(completion.values().getFirst(), is("Frank-birthday"));
265+
}
266+
267+
private int sortArguments(McpSchema.PromptArgument first, McpSchema.PromptArgument second) {
268+
return first.name().compareTo(second.name());
269+
}
270+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.extensions.mcp.examples.calendar.declarative;
18+
19+
import io.helidon.webserver.WebServer;
20+
import io.helidon.webserver.testing.junit5.ServerTest;
21+
22+
import io.modelcontextprotocol.client.McpClient;
23+
import io.modelcontextprotocol.client.McpSyncClient;
24+
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
25+
26+
@ServerTest
27+
class SseClientTest extends BaseTest {
28+
private final McpSyncClient client;
29+
30+
SseClientTest(WebServer server) {
31+
this.client = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + server.port())
32+
.sseEndpoint("/calendar")
33+
.build()).build();
34+
client.initialize();
35+
}
36+
37+
@Override
38+
McpSyncClient client() {
39+
return client;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.extensions.mcp.examples.calendar.declarative;
18+
19+
import io.helidon.webserver.WebServer;
20+
import io.helidon.webserver.testing.junit5.ServerTest;
21+
22+
import io.modelcontextprotocol.client.McpClient;
23+
import io.modelcontextprotocol.client.McpSyncClient;
24+
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
25+
26+
@ServerTest
27+
class StreamableHttpClientTest extends BaseTest {
28+
private final McpSyncClient client;
29+
30+
StreamableHttpClientTest(WebServer server) {
31+
this.client = McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + server.port())
32+
.endpoint("/calendar")
33+
.build()).build();
34+
client.initialize();
35+
}
36+
37+
@Override
38+
McpSyncClient client() {
39+
return client;
40+
}
41+
}

0 commit comments

Comments
 (0)