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
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.extensions.mcp.examples.calendar;

import java.util.List;
import java.util.function.Function;

import io.helidon.extensions.mcp.server.McpParameters;
import io.helidon.extensions.mcp.server.McpRequest;
import io.helidon.extensions.mcp.server.McpTool;
import io.helidon.extensions.mcp.server.McpToolContent;
import io.helidon.extensions.mcp.server.McpToolContents;

/**
* MCP tool to list calendar events. Available as an alternative to using
* resources.
*/
final class ListCalendarEventTool implements McpTool {
private static final String SCHEMA = """
{
"type": "object",
"description": "List calendar events",
"properties": {
"date": {
"description": "Event date in the following format YYYY-MM-DD",
"type": "string"
}
}
}
""";

private final Calendar calendar;

ListCalendarEventTool(Calendar calendar) {
this.calendar = calendar;
}

@Override
public String name() {
return "list-calendar-event";
}

@Override
public String description() {
return "List calendar events.";
}

@Override
public String schema() {
return SCHEMA;
}

@Override
public Function<McpRequest, List<McpToolContent>> tool() {
return this::listCalendarEvents;
}

private List<McpToolContent> listCalendarEvents(McpRequest request) {
McpParameters mcpParameters = request.parameters();

String date = mcpParameters.get("date")
.asString()
.orElse(null);

String entries = calendar.readContentMatchesLine(line -> date == null || line.contains(date));
return List.of(McpToolContents.textContent(entries));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ static void setUpRoute(HttpRouting.Builder builder) {
McpServerConfig.builder()
.config(config.get("mcp.server"))
.addTool(new AddCalendarEventTool(calendar))
.addTool(new ListCalendarEventTool(calendar))
.addResource(new CalendarEventResource(calendar))
.addPrompt(new CreateCalendarEventPrompt())
.addCompletion(new CreateCalendarEventPromptCompletion())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

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

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

McpSchema.JsonSchema schema = tool.inputSchema();
assertThat(schema.type(), is("object"));
assertThat(schema.properties().keySet(), hasItems("name", "date", "attendees"));
McpSchema.JsonSchema schema1 = tool1.inputSchema();
assertThat(schema1.type(), is("object"));
assertThat(schema1.properties().keySet(), hasItems("name", "date", "attendees"));

McpSchema.Tool tool2 = tools.getLast();
assertThat(tool2.name(), is("list-calendar-event"));
assertThat(tool2.description(), is("List calendar events."));

McpSchema.JsonSchema schema2 = tool2.inputSchema();
assertThat(schema2.type(), is("object"));
assertThat(schema2.properties().keySet(), hasItems("date"));
}

@Test
@Order(2)
void testToolCall() {
Map<String, Object> arguments = Map.of("name", "Frank-birthday", "date", "2021-04-20", "attendees", List.of("Frank"));
void testAddToolCall() {
Map<String, Object> arguments = Map.of("name", "Frank-birthday",
"date", "2021-04-20",
"attendees", List.of("Frank"));
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("add-calendar-event", arguments);
McpSchema.CallToolResult result = client().callTool(request);
assertThat(result.isError(), is(false));
Expand All @@ -86,6 +95,25 @@ void testToolCall() {

@Test
@Order(3)
void testListToolCall() {
Map<String, Object> arguments = Map.of("date", "2021-04-20");
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("list-calendar-event", arguments);
McpSchema.CallToolResult result = client().callTool(request);
assertThat(result.isError(), is(false));

List<McpSchema.Content> contents = result.content();
assertThat(contents.size(), is(1));

McpSchema.Content content = contents.getFirst();
assertThat(content.type(), is("text"));
assertThat(content, instanceOf(McpSchema.TextContent.class));

McpSchema.TextContent textContent = (McpSchema.TextContent) content;
assertThat(textContent.text(), containsString("Frank-birthday"));
}

@Test
@Order(4)
void testPromptList() {
McpSchema.ListPromptsResult listPrompt = client().listPrompts();
List<McpSchema.Prompt> prompts = listPrompt.prompts();
Expand Down Expand Up @@ -116,7 +144,7 @@ void testPromptList() {
}

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

@Test
@Order(5)
@Order(6)
void testResourceList() {
McpSchema.ListResourcesResult result = client().listResources();
List<McpSchema.Resource> resources = result.resources();
Expand All @@ -152,7 +180,7 @@ void testResourceList() {
}

@Test
@Order(6)
@Order(7)
void testResourceCall() {
String uri = client().listResources().resources().getFirst().uri();
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest(uri);
Expand All @@ -171,7 +199,7 @@ void testResourceCall() {
}

@Test
@Order(7)
@Order(8)
void testResourceTemplateList() {
McpSchema.ListResourceTemplatesResult result = client().listResourceTemplates();
List<McpSchema.ResourceTemplate> templates = result.resourceTemplates();
Expand All @@ -185,7 +213,7 @@ void testResourceTemplateList() {
}

@Test
@Order(8)
@Order(9)
void testResourceTemplateCall() {
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("file://events/Frank-birthday");
McpSchema.ReadResourceResult result = client().readResource(request);
Expand All @@ -202,7 +230,7 @@ void testResourceTemplateCall() {
}

@Test
@Order(9)
@Order(10)
void testCalendarEventPromptCompletion() {
McpSchema.CompleteRequest request1 = new McpSchema.CompleteRequest(
new McpSchema.PromptReference("create-event"),
Expand Down Expand Up @@ -231,7 +259,7 @@ void testCalendarEventPromptCompletion() {
}

@Test
@Order(10)
@Order(11)
void testCalendarEventResourceCompletion() {
McpSchema.CompleteRequest request = new McpSchema.CompleteRequest(
new McpSchema.ResourceReference(URI_TEMPLATE),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#
# Copyright (c) 2025 Oracle and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
handlers=io.helidon.logging.jul.HelidonConsoleHandler

# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread
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

.level=INFO
io.helidon.extensions.mcp.server.McpServerFeature.level=FINEST
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package io.helidon.extensions.mcp.server;

import java.io.ByteArrayOutputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -28,11 +30,17 @@
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonReaderFactory;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;
import jakarta.json.JsonWriter;
import jakarta.json.JsonWriterFactory;
import jakarta.json.stream.JsonGenerator;

final class McpJsonRpc {
static final JsonBuilderFactory JSON_BUILDER_FACTORY = Json.createBuilderFactory(Map.of());
static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(Map.of());
static final JsonWriterFactory JSON_PP_WRITER_FACTORY = Json.createWriterFactory(
Map.of(JsonGenerator.PRETTY_PRINTING, true));

private static final Map<String, JsonObject> CACHE = new ConcurrentHashMap<>();
private static final JsonObject EMPTY_OBJECT_SCHEMA = JSON_BUILDER_FACTORY.createObjectBuilder()
Expand Down Expand Up @@ -481,4 +489,12 @@ static JsonObject toJson(McpCompletionContent content) {
static JsonObject disconnectSession() {
return JSON_BUILDER_FACTORY.createObjectBuilder().add("disconnect", true).build();
}

static String prettyPrint(JsonStructure json) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (JsonWriter writer = JSON_PP_WRITER_FACTORY.createWriter(baos)) {
writer.write(json);
}
return baos.toString(StandardCharsets.UTF_8);
}
}
Loading