Skip to content

Commit 0b68ac6

Browse files
authored
Improve logging in server and some updates for demo (#76)
* Improve logging in server by including request and using a JSON pretty printer * Add new tool to Calendar * Log exception in McpSession to avoid warnings while running some tests
1 parent 629e1e4 commit 0b68ac6

File tree

7 files changed

+196
-32
lines changed

7 files changed

+196
-32
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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;
18+
19+
import java.util.List;
20+
import java.util.function.Function;
21+
22+
import io.helidon.extensions.mcp.server.McpParameters;
23+
import io.helidon.extensions.mcp.server.McpRequest;
24+
import io.helidon.extensions.mcp.server.McpTool;
25+
import io.helidon.extensions.mcp.server.McpToolContent;
26+
import io.helidon.extensions.mcp.server.McpToolContents;
27+
28+
/**
29+
* MCP tool to list calendar events. Available as an alternative to using
30+
* resources.
31+
*/
32+
final class ListCalendarEventTool implements McpTool {
33+
private static final String SCHEMA = """
34+
{
35+
"type": "object",
36+
"description": "List calendar events",
37+
"properties": {
38+
"date": {
39+
"description": "Event date in the following format YYYY-MM-DD",
40+
"type": "string"
41+
}
42+
}
43+
}
44+
""";
45+
46+
private final Calendar calendar;
47+
48+
ListCalendarEventTool(Calendar calendar) {
49+
this.calendar = calendar;
50+
}
51+
52+
@Override
53+
public String name() {
54+
return "list-calendar-event";
55+
}
56+
57+
@Override
58+
public String description() {
59+
return "List calendar events.";
60+
}
61+
62+
@Override
63+
public String schema() {
64+
return SCHEMA;
65+
}
66+
67+
@Override
68+
public Function<McpRequest, List<McpToolContent>> tool() {
69+
return this::listCalendarEvents;
70+
}
71+
72+
private List<McpToolContent> listCalendarEvents(McpRequest request) {
73+
McpParameters mcpParameters = request.parameters();
74+
75+
String date = mcpParameters.get("date")
76+
.asString()
77+
.orElse(null);
78+
79+
String entries = calendar.readContentMatchesLine(line -> date == null || line.contains(date));
80+
return List.of(McpToolContents.textContent(entries));
81+
}
82+
}

examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Main.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ static void setUpRoute(HttpRouting.Builder builder) {
5252
McpServerConfig.builder()
5353
.config(config.get("mcp.server"))
5454
.addTool(new AddCalendarEventTool(calendar))
55+
.addTool(new ListCalendarEventTool(calendar))
5556
.addResource(new CalendarEventResource(calendar))
5657
.addPrompt(new CreateCalendarEventPrompt())
5758
.addCompletion(new CreateCalendarEventPromptCompletion())

examples/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/BaseTest.java

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import static org.hamcrest.Matchers.hasItems;
3737
import static org.hamcrest.Matchers.instanceOf;
3838
import static org.hamcrest.Matchers.is;
39-
import static org.hamcrest.Matchers.nullValue;
4039
import static org.hamcrest.Matchers.startsWith;
4140

4241
@TestMethodOrder(OrderAnnotation.class)
@@ -54,21 +53,31 @@ static void routing(HttpRouting.Builder builder) {
5453
void testToolList() {
5554
McpSchema.ListToolsResult listTool = client().listTools();
5655
List<McpSchema.Tool> tools = listTool.tools();
57-
assertThat(tools.size(), is(1));
56+
assertThat(tools.size(), is(2));
5857

59-
McpSchema.Tool tool = tools.getFirst();
60-
assertThat(tool.name(), is("add-calendar-event"));
61-
assertThat(tool.description(), is("Adds a new event to the calendar."));
58+
McpSchema.Tool tool1 = tools.getFirst();
59+
assertThat(tool1.name(), is("add-calendar-event"));
60+
assertThat(tool1.description(), is("Adds a new event to the calendar."));
6261

63-
McpSchema.JsonSchema schema = tool.inputSchema();
64-
assertThat(schema.type(), is("object"));
65-
assertThat(schema.properties().keySet(), hasItems("name", "date", "attendees"));
62+
McpSchema.JsonSchema schema1 = tool1.inputSchema();
63+
assertThat(schema1.type(), is("object"));
64+
assertThat(schema1.properties().keySet(), hasItems("name", "date", "attendees"));
65+
66+
McpSchema.Tool tool2 = tools.getLast();
67+
assertThat(tool2.name(), is("list-calendar-event"));
68+
assertThat(tool2.description(), is("List calendar events."));
69+
70+
McpSchema.JsonSchema schema2 = tool2.inputSchema();
71+
assertThat(schema2.type(), is("object"));
72+
assertThat(schema2.properties().keySet(), hasItems("date"));
6673
}
6774

6875
@Test
6976
@Order(2)
70-
void testToolCall() {
71-
Map<String, Object> arguments = Map.of("name", "Frank-birthday", "date", "2021-04-20", "attendees", List.of("Frank"));
77+
void testAddToolCall() {
78+
Map<String, Object> arguments = Map.of("name", "Frank-birthday",
79+
"date", "2021-04-20",
80+
"attendees", List.of("Frank"));
7281
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("add-calendar-event", arguments);
7382
McpSchema.CallToolResult result = client().callTool(request);
7483
assertThat(result.isError(), is(false));
@@ -86,6 +95,25 @@ void testToolCall() {
8695

8796
@Test
8897
@Order(3)
98+
void testListToolCall() {
99+
Map<String, Object> arguments = Map.of("date", "2021-04-20");
100+
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("list-calendar-event", arguments);
101+
McpSchema.CallToolResult result = client().callTool(request);
102+
assertThat(result.isError(), is(false));
103+
104+
List<McpSchema.Content> contents = result.content();
105+
assertThat(contents.size(), is(1));
106+
107+
McpSchema.Content content = contents.getFirst();
108+
assertThat(content.type(), is("text"));
109+
assertThat(content, instanceOf(McpSchema.TextContent.class));
110+
111+
McpSchema.TextContent textContent = (McpSchema.TextContent) content;
112+
assertThat(textContent.text(), containsString("Frank-birthday"));
113+
}
114+
115+
@Test
116+
@Order(4)
89117
void testPromptList() {
90118
McpSchema.ListPromptsResult listPrompt = client().listPrompts();
91119
List<McpSchema.Prompt> prompts = listPrompt.prompts();
@@ -116,7 +144,7 @@ void testPromptList() {
116144
}
117145

118146
@Test
119-
@Order(4)
147+
@Order(5)
120148
void testPromptCall() {
121149
Map<String, Object> arguments = Map.of("name", "Frank-birthday", "date", "2021-04-20", "attendees", "Frank");
122150
McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("create-event", arguments);
@@ -138,7 +166,7 @@ void testPromptCall() {
138166
}
139167

140168
@Test
141-
@Order(5)
169+
@Order(6)
142170
void testResourceList() {
143171
McpSchema.ListResourcesResult result = client().listResources();
144172
List<McpSchema.Resource> resources = result.resources();
@@ -152,7 +180,7 @@ void testResourceList() {
152180
}
153181

154182
@Test
155-
@Order(6)
183+
@Order(7)
156184
void testResourceCall() {
157185
String uri = client().listResources().resources().getFirst().uri();
158186
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest(uri);
@@ -171,7 +199,7 @@ void testResourceCall() {
171199
}
172200

173201
@Test
174-
@Order(7)
202+
@Order(8)
175203
void testResourceTemplateList() {
176204
McpSchema.ListResourceTemplatesResult result = client().listResourceTemplates();
177205
List<McpSchema.ResourceTemplate> templates = result.resourceTemplates();
@@ -185,7 +213,7 @@ void testResourceTemplateList() {
185213
}
186214

187215
@Test
188-
@Order(8)
216+
@Order(9)
189217
void testResourceTemplateCall() {
190218
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("file://events/Frank-birthday");
191219
McpSchema.ReadResourceResult result = client().readResource(request);
@@ -202,7 +230,7 @@ void testResourceTemplateCall() {
202230
}
203231

204232
@Test
205-
@Order(9)
233+
@Order(10)
206234
void testCalendarEventPromptCompletion() {
207235
McpSchema.CompleteRequest request1 = new McpSchema.CompleteRequest(
208236
new McpSchema.PromptReference("create-event"),
@@ -231,7 +259,7 @@ void testCalendarEventPromptCompletion() {
231259
}
232260

233261
@Test
234-
@Order(10)
262+
@Order(11)
235263
void testCalendarEventResourceCompletion() {
236264
McpSchema.CompleteRequest request = new McpSchema.CompleteRequest(
237265
new McpSchema.ResourceReference(URI_TEMPLATE),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
handlers=io.helidon.logging.jul.HelidonConsoleHandler
17+
18+
# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread
19+
java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n
20+
21+
.level=INFO
22+
io.helidon.extensions.mcp.server.McpServerFeature.level=FINEST

server/src/main/java/io/helidon/extensions/mcp/server/McpJsonRpc.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package io.helidon.extensions.mcp.server;
1818

19+
import java.io.ByteArrayOutputStream;
1920
import java.io.StringReader;
21+
import java.nio.charset.StandardCharsets;
2022
import java.util.List;
2123
import java.util.Map;
2224
import java.util.Set;
@@ -28,11 +30,17 @@
2830
import jakarta.json.JsonObject;
2931
import jakarta.json.JsonObjectBuilder;
3032
import jakarta.json.JsonReaderFactory;
33+
import jakarta.json.JsonStructure;
3134
import jakarta.json.JsonValue;
35+
import jakarta.json.JsonWriter;
36+
import jakarta.json.JsonWriterFactory;
37+
import jakarta.json.stream.JsonGenerator;
3238

3339
final class McpJsonRpc {
3440
static final JsonBuilderFactory JSON_BUILDER_FACTORY = Json.createBuilderFactory(Map.of());
3541
static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(Map.of());
42+
static final JsonWriterFactory JSON_PP_WRITER_FACTORY = Json.createWriterFactory(
43+
Map.of(JsonGenerator.PRETTY_PRINTING, true));
3644

3745
private static final Map<String, JsonObject> CACHE = new ConcurrentHashMap<>();
3846
private static final JsonObject EMPTY_OBJECT_SCHEMA = JSON_BUILDER_FACTORY.createObjectBuilder()
@@ -481,4 +489,12 @@ static JsonObject toJson(McpCompletionContent content) {
481489
static JsonObject disconnectSession() {
482490
return JSON_BUILDER_FACTORY.createObjectBuilder().add("disconnect", true).build();
483491
}
492+
493+
static String prettyPrint(JsonStructure json) {
494+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
495+
try (JsonWriter writer = JSON_PP_WRITER_FACTORY.createWriter(baos)) {
496+
writer.write(json);
497+
}
498+
return baos.toString(StandardCharsets.UTF_8);
499+
}
484500
}

0 commit comments

Comments
 (0)