diff --git a/docs/README.md b/docs/README.md index 3a7ba48b..3496f779 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,8 @@ # Helidon MCP Documentation -[MCP for Helidon SE](mcp/README.md) - -[MCP for Helidon SE Declarative](mcp-declarative/README.md) +This documentation set covers both imperative and declarative APIs for building MCP servers with Helidon. +- [MCP for Helidon SE](mcp/README.md) +- [MCP for Helidon SE Declarative](mcp-declarative/README.md) +- [MCP Migration Guide 1.1](mcp/migration_guide_1.1.md) +- [MCP Declarative Migration Guide 1.1](mcp-declarative/migration_guide_1.1.md) diff --git a/docs/mcp-declarative/README.md b/docs/mcp-declarative/README.md index f93c2365..ef82cdd4 100644 --- a/docs/mcp-declarative/README.md +++ b/docs/mcp-declarative/README.md @@ -1,14 +1,13 @@ # Helidon MCP Extension -Helidon support for the Model Context Protocol (MCP). +Server-side Helidon support for the Model Context Protocol (MCP). ## Overview -The Model Context Protocol (MCP) defines a standard communication method that enables LLMs (Large Language Models) to interact -with both internal and external data sources. More than just a protocol, MCP establishes a connected environment of AI agents -capable of accessing real-time information. MCP follows a client-server architecture: clients, typically used by AI agents, -initiate communication, while servers manage access to data sources and provide data retrieval capabilities. Helidon offers -server-side support and can be accessed by any client that implements the MCP specification. +The Model Context Protocol (MCP) defines a standardized way for LLMs (Large Language Models) to interact with internal and +external data sources. MCP uses a client-server architecture in which clients (typically AI agents) initiate communication and +servers expose capabilities for data access, retrieval, and interaction. Helidon provides MCP server-side support that can be +consumed by any client implementing the MCP specification. ## Maven Coordinates @@ -40,16 +39,16 @@ Also include the following annotation processor setup: ## Usage -This section walks you through creating and configuring various MCP components. +This section describes how to create and configure core MCP components in Helidon. ### MCP Server -Servers provide the fundamental building blocks for adding context to language models via MCP. Clients discover a server via a -configurable `HTTP` endpoint. Servers manage connections and support features detailed later in this guide. Helidon represents an -MCP server as an `HttpFeature`, which is registered as part of your web server’s routing. You can create multiple MCP servers by -defining multiple classes annotated with `@Mcp.Server`, each using a distinct `@Mcp.Path`. Each path must be unique and serves as -the client’s entry point. Helidon imposes no restrictions on naming or versioning; those values are simply shared with the client -upon connection. +Servers provide the primary integration point for adding context to language models through MCP. Clients discover a server via a +configurable HTTP endpoint. Servers manage client connections and expose the capabilities described later in this guide. Helidon +represents an MCP server as an `HttpFeature`, registered as part of web server routing. You can host multiple MCP servers by +defining multiple classes annotated with `@Mcp.Server`, each using a distinct `@Mcp.Path`. Each path must be unique and serves as +an independent entry point for MCP clients. Helidon does not enforce naming or versioning conventions; these values are shared +with the client during connection initialization. ```java @Mcp.Server @@ -73,18 +72,19 @@ class McpServer { ### Tool -`Tools` enable models to interact with external systems, such as querying databases, calling APIs, or performing computations. -Define a tool by annotating a method with `@Mcp.Tool`. Method names become tool names unless overridden with`@Mcp.Name`. Input -schemas are generated using [JSON Schema Specification](https://json-schema.org/specification); Helidon auto-generates schemas -when inputs are primitive types (non-POJO). `Tools` are automatically registered when defined within a server class. +`Tools` enable models to interact with external systems, such as querying databases, calling APIs, or performing computations. +Define a tool by annotating a method with `@Mcp.Tool`. Method names become tool names unless overridden with `@Mcp.Name`. Input +schemas are generated using the [JSON Schema Specification](https://json-schema.org/specification); Helidon auto generates schemas when inputs are primitive types +(non-POJO). `Tools` are automatically registered when defined within a server class. You can inject `McpToolRequest` as a +parameter to your tool method. It extends `McpRequest` and provides access to the `McpTool` instance via the `tool()` method. ```java @Mcp.Server class Server { @Mcp.Tool("Tool description") - List myToolMethod(String input) { - return List.of(McpToolContents.textContent("Input: " + input)); + McpToolResult myToolMethod(String input) { + return McpToolResult.create("Input: " + input); } } ``` @@ -100,11 +100,11 @@ class Server { @Mcp.Tool("Tool description") @Mcp.Name("MyTool") - List myToolMethod(Coordinate coordinate) { + McpToolResult myToolMethod(Coordinate coordinate) { String result = String.format("latitude: %s, longitude: %s", coordinate.latitude(), - coordinate.latitude()); - return List.of(McpToolContents.textContent(result)); + coordinate.longitude()); + return McpToolResult.builder().addTextContent(result).build(); } @JsonSchema.Schema @@ -113,25 +113,67 @@ class Server { } ``` +#### Structured content and output schema + +Structured content is returned as a JSON object in the `structuredContent` field of a result. For backwards compatibility, +a tool that returns structured content SHOULD also return the serialized JSON in a text content block. If there is no content +added to the `McpToolResult` builder, Helidon will serialize the structured content and add it by itself. +Tools have to provide an output schema for validation of structured results if it is using structured content. + +To add an output schema to the tool, use the `@Mcp.ToolOutputSchema` or `@Mcp.ToolOutputSchemaText` annotations: + +- **`@Mcp.ToolOutputSchema`**: Defines the output schema using a class (POJO). Helidon will generate the JSON schema from the class. +- **`@Mcp.ToolOutputSchemaText`**: Defines the output schema using a literal JSON schema string. + +```java +@Mcp.Tool("Tool returns a structured content") +@Mcp.ToolOutputSchema(Foo.class) +McpToolResult toolWithSchema(McpToolRequest request) { + return McpToolResult.builder() + .structuredContent(new Foo("bar")) + .build(); +} + +@Mcp.Tool("Tool returns a structured content with text schema") +@Mcp.ToolOutputSchemaText("{\"type\": \"object\", \"properties\": {\"value\": {\"type\": \"string\"}}}") +McpToolResult toolWithTextSchema(McpToolRequest request) { + return McpToolResult.builder() + .structuredContent(new Foo("bar")) + .build(); +} + +@JsonSchema.Schema +public record Foo(String bar) { +} +``` + #### JSON Schema Use JSON Schema to validate and describe input parameters and their structure. You can define schemas via the `@JsonSchema.Schema` annotation. The complete documentation is available on [Helidon documentation](https://helidon.io/docs/v4/se/json/schema). -#### Tool Content Types +#### Tool Result -Helidon supports three types of tool output: +Helidon supports six types of tool result content: - **Text**: Text content with the default `text/plain` media type. - **Image**: Image content with a custom media type. -- **Resource**: A reference to an `McpResource` via a URI (must be registered on the server). +- **Audio**: Audio content with a custom media type. +- **Resource links**: A reference to a resource. +- **Text Resource**: Text resource content with the default `text/plain` media type. +- **Binary Resource**: Binary resource content with a custom media type. -Use the `McpToolContents` factory to create tool contents: +Use the `McpToolResult` builder to create tool contents: ```java -McpToolContent text = McpToolContents.textContent("text"); -McpToolContent resource = McpToolContents.resourceContent("http://path"); -McpToolContent image = McpToolContents.imageContent("base64", MediaTypes.create("image/png")); +McpToolResult result = McpToolResult.builder() + .addTextContent("text") + .addImageContent(pngImageBytes(), MediaTypes.create("image/png")) + .addAudioContent(wavAudioBytes(), MediaTypes.create("audio/wav")) + .addResourceLinkContent("name", "https://foo") + .addTextResourceContent("text") + .addBinaryResourceContent(gzipBytes(), MediaTypes.create("application/gzip")) + .build(); ``` ### Prompt @@ -144,9 +186,9 @@ the method’s name and `@Mcp.Role` to specify the speaker for text-only prompts @Mcp.Server class Server { - @Mcp.Prompt("Prompt description") - List myPromptMethod(String argument) { - return List.of(McpPromptContents.textContent(argument + ".", McpRole.USER)); + @Mcp.Prompt("Echo Prompt") + McpPromptResult echoPrompt(String argument) { + return McpPromptResult.create(argument); } } ``` @@ -164,33 +206,39 @@ class Server { @Mcp.Prompt("Prompt description") @Mcp.Name("MyPrompt") @Mcp.Role(McpRole.USER) - String myPromptMethod(@Mcp.Description("Argument description") String argument) { - return argument + "."; + String echoPrompt(@Mcp.Description("Argument description") String argument) { + return argument; } } ``` -#### Prompt Content Types +#### Prompt Result -Helidon supports three prompt content types: +Five prompt result content types can be created: - **Text**: Text content with a default `text/plain` media type. - **Image**: Image content with a custom media type. -- **Resource**: URI references to `McpResource` instances. +- **Audio**: Audio content with a custom media type. +- **Text Resource**: Text resource content with a default `text/plain` media type. +- **Binary Resource**: Binary resource content with a custom media type. -`Prompt` content can be created using `McpPromptContents` factory, and used as result of the `Prompt` execution. +Create prompt content with the `McpPromptResult` builder: ```java -McpPromptContent text = McpPromptContents.textContent("text", Role.USER); -McpPromptContent resource = McpPromptContents.resourceContent("http://path", Role.USER); -McpPromptContent image = McpPromptContents.imageContent("base64", MediaTypes.create("image/png"), Role.USER); +McpPromptResult result = McpPromptResult.builder() + .addTextContent("text") + .addImageContent(pngImageBytes(), MediaTypes.create("image/png")) + .addAudioContent(wavAudioBytes(), MediaTypes.create("audio/wav")) + .addTextResourceContent("text") + .addBinaryResourceContent(gzipBytes(), MediaTypes.create("application/gzip")) + .build(); ``` ### Resource `Resources` allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Clients can list and read them. Resources are identified by name, description, and media type. -Define resources using `@Mcp.Resource`: +Define resources using `@Mcp.Resource`. ```java @Mcp.Server @@ -200,8 +248,8 @@ class Server { uri = "file://path", description = "Resource description", mediaType = MediaTypes.TEXT_PLAIN_VALUE) - List resource() { - return List.of(McpResourceContents.textContent("text")); + McpResourceResult resource() { + return McpResourceResult.create("text"); } } ``` @@ -209,6 +257,7 @@ class Server { #### Configuration Use `String` return types for text-only resources. The `@Mcp.Name` annotation lets you override the default resource name. +You can inject `McpResourceRequest` as a parameter to your resource method. ```java @Mcp.Server @@ -219,7 +268,7 @@ class Server { description = "Resource description", mediaType = MediaTypes.TEXT_PLAIN_VALUE) @Mcp.Name("MyResource") - String resource(McpRequest request) { + String resource(McpResourceRequest request) { return "text"; } } @@ -251,18 +300,20 @@ class Server { } ``` -#### Resource Content Types +#### Resource Result -Helidon supports two resource content types: +Helidon supports two resource result content types: - **Text**: Text content with a default `text/plain` media type. - **Binary**: Binary content with a custom media type. -`Resource` content can be created using `McpResourceContents` factory: +Create resource content with the `McpResourceResult` builder: ```java -McpResourceContent text = McpResourceContents.textContent("data"); -McpResourceContent binary = McpResourceContents.binaryContent("{\"foo\":\"bar\"}", MediaTypes.APPLICATION_JSON); +McpResourceResult text = McpResourceResult.create("data"); +McpResourceResult binary = McpResourceResult.builder() + .addBinaryContent(data, MediaTypes.APPLICATION_JSON) + .build(); ``` ### Resource Subscribers @@ -274,13 +325,13 @@ longer interested in receiving update notifications, it can issue an unsubscribe request. Generally, the MCP server processes subscribe and unsubscribe requests without -any user-provided code executed on the server side. Clients simply subscribe -and unsubscribe (within the same session) using the resource URI and updates -are propagated to all active subscribers in all sessions. -Helidon MCP supports server-side subscribers and unsubscribers in case logic needs -to be executed server side to handle those events --for example, a subscription -may start a thread to monitor database updates and stop it when the unsubscription -arrives. +any user-provided code executed on the server side. Clients subscribe +and unsubscribe (within the same session) using the resource URI, and updates +are propagated to all active subscribers in all sessions. +Helidon MCP supports server-side subscribers and unsubscribers when custom logic must +be executed on the server to handle those events (for example, a subscription may +start a thread to monitor database updates and stop it when the unsubscription +arrives). The following example shows our resource example together with a server-side subscriber and unsubscriber: @@ -299,45 +350,46 @@ class McpSubscribersServer { } @Mcp.ResourceSubscriber("http://dbtable") - void subscribe(McpRequest request) { + void subscribe(McpSubscribeRequest request) { startDbTableMonitor(); } @Mcp.ResourceUnsubscriber("http://dbtable") - void unsubscribe(McpRequest request) { + void unsubscribe(McpUnsubscribeRequest request) { stopDbTableMonitor(); } } ``` MCP subscriptions are available via the injectable _features_ instance and can -send notifications manually as follows: +send notifications manually: ```java @Mcp.ResourceSubscriber("http://dbtable") -void subscribe(McpRequest request, McpFeatures features) { +void subscribe(McpSubscribeRequest request, McpFeatures features) { if (wasUpdated()) { features.subscriptions().sendUpdate("http://dbtable"); } } ``` -MCP clients will automatically issue a resource read everytime an update notification +MCP clients automatically issue a resource read every time an update notification arrives. ### Completion The `Completion` feature offers auto-suggestions for prompt arguments or resource template parameters, making the server easier -to use and explore. Bind completions to prompts (by name) or resource templates (by URI) using `@Mcp.Completion`: +to use and explore. Bind completions to prompts (by name) or resource templates (by URI) using `@Mcp.Completion`. You can inject +`McpCompletionRequest` as a parameter to your completion method. It extends `McpRequest` and provides `name()` and `value()` +methods to get the argument name and its current value. ```java @Mcp.Server class Server { @Mcp.Completion("create") - McpCompletionContent completionPromptArgument(McpRequest request) { - String value = request.parameters().get("value").asString().orElse(null); - return McpCompletionContents.completion(value + "."); + McpCompletionResult completionPromptArgument(McpCompletionRequest request) { + return McpCompletionResult.create(request.value() + "."); } } ``` @@ -350,9 +402,8 @@ The default type of completion is prompt. When creating a completion for a resou class Server { @Mcp.Completion(value = "resource/{path1}", type = McpCompletionType.RESOURCE) - McpCompletionContent completionPromptArgument(McpRequest request) { - String value = request.parameters().get("value").asString().orElse(null); - return McpCompletionContents.completion(value + "."); + McpCompletionResult completionPromptArgument(McpCompletionRequest request) { + return McpCompletionResult.create(request.value() + "."); } } ``` @@ -372,27 +423,105 @@ class Server { } ``` -#### Completion Content Type +#### Completion Result Completion content results in a list of suggestions: ```java -McpCompletionContent content = McpCompletionContents.completion("suggestion"); +McpCompletionResult result = McpCompletionResult.create("suggestion1", "suggestion2"); ``` ## MCP Parameters -Client parameters are available in `McpTool`, `McpPrompt`, and `McpCompletion` business logic via the `McpParameters` API: +Client parameters are available in `McpTool`, `McpPrompt`, and `McpCompletion` business logic via the `McpParameters` API. +This class provides a flexible way to access and convert parameters from the client request. + +### Basic Usage + +You can access parameters by their key and convert them to various types. ```java -void process(McpRequest request) { +void process(McpToolRequest request) { McpParameters parameters = request.parameters(); - parameters.get("list").asList().get(); - parameters.get("age").asInteger().orElse(18); - parameters.get("authorized").asBoolean().orElse(false); - parameters.get("name").asString().orElse("defaultName"); - parameters.get("address").as(Address.class).orElseThrow(); + // Access nested parameters + McpParameters address = parameters.get("address"); + + // Convert to primitive types + String name = parameters.get("name").asString().orElse("defaultName"); + int age = parameters.get("age").asInteger().orElse(18); + boolean authorized = parameters.get("authorized").asBoolean().orElse(false); + + // Convert to a list of strings + List roles = parameters.get("roles").asList(String.class).orElse(List.of()); + + // Convert to a custom POJO + Address homeAddress = address.as(Address.class).orElseThrow(); +} +``` + +### Checking for Presence + +You can check if a parameter is present or empty. + +```java +void parameters(McpRequest request) { + McpParameters param = request.parameters().get("optionalParam"); + if (param.isPresent()) { + // ... + } + if (param.isEmpty()) { + // ... + } +} +``` + +### Advanced Conversions + +The `McpParameters` API also supports more advanced conversions. + +```java +void convert(McpToolRequest request) { + // Convert to a list of McpParameters to iterate over a JSON array + List items = request.parameters().get("items").asList().get(); + for (McpParameters item : items) { + String itemName = item.get("name").asString().get(); + double itemPrice = item.get("price").asDouble().get(); + } + + // Convert to a map + Map metadata = request.parameters().get("metadata").asMap().get(); +} +``` + +## McpRequest + +The `McpRequest` object is the base interface for every request, providing access to client-side data and features. + +- **`parameters()`**: Returns `McpParameters` for accessing client-provided parameters. +- **`meta()`**: Returns `McpParameters` for accessing client-provided metadata. +- **`features()`**: Returns `McpFeatures` for accessing advanced features such as logging, progress, cancellation, elicitation, sampling, and roots. +- **`protocolVersion()`**: Returns the negotiated protocol version between the server and the client. +- **`sessionContext()`**: Returns a `Context` for session-scoped data. +- **`requestContext()`**: Returns a `Context` for request-scoped data. + +## Context Management + +The `McpRequest` provides access to two types of context: + +- **Session Context**: Used to store data that persists throughout the duration of the client's session. +- **Request Context**: Used to store data specific to the current request. + +You can access these by adding `McpRequest` as a parameter to your tool or prompt method. + +```java +@Mcp.Tool("Tool with state") +McpToolResult toolWithState(McpRequest request) { + Context sessionContext = request.sessionContext(); + int callCount = sessionContext.get("callCount", Integer.class).orElse(0); + sessionContext.register("callCount", ++callCount); + + return McpToolResult.create("This tool has been called " + callCount + " times in this session."); } ``` @@ -410,7 +539,7 @@ directly to clients: class Server { @Mcp.Tool("Tool description") - List getLocationCoordinates(McpFeatures features) { + McpToolResult getLocationCoordinates(McpFeatures features) { McpLogger logger = features.logger(); logger.info("Logging info"); @@ -421,7 +550,7 @@ class Server { logger.critical("Critical issue"); logger.alert("Alert message"); - return List.of(McpToolContents.textContent("Text")); + return McpToolResult.create(); } } ``` @@ -435,14 +564,14 @@ For long-running tasks, clients can request progress updates. Use `McpProgress` class Server { @Mcp.Tool("Tool description") - List getLocationCoordinates(McpFeatures features) { + McpToolResult getLocationCoordinates(McpFeatures features) { McpProgress progress = features.progress(); progress.total(100); for (int i = 1; i <= 10; i++) { longRunningTask(); progress.send(i * 10); } - return List.of(McpToolContents.textContent("text")); + return McpToolResult.create(); } } ``` @@ -475,20 +604,60 @@ Example of a Tool checking for cancellation request. ```java @Mcp.Tool("Cancellation Tool") -List cancellationTool(McpCancellation cancellation) { +McpToolResult cancellationTool(McpCancellation cancellation) { long now = System.currentTimeMillis(); long timeout = now + TimeUnit.SECONDS.toMillis(5); - McpCancellation cancellation = request.features().cancellation(); while (now < timeout) { if (cancellation.verify().isRequested()) { String reason = cancellation.verify().reason(); - return List.of(McpToolContents.textContent(reason)); + return McpToolResult.create(reason); } longRunningOperation(); now = System.currentTimeMillis(); } - return List.of(McpToolContents.textContent("text")); + return McpToolResult.create(); +} +``` + +### Elicitation + +See the full [elicitation documentation details](../mcp/README.md#elicitation) + +#### Example + +```java +@Mcp.Tool("Collect additional user input from the connected client.") +McpToolResult elicitationTool(McpElicitation elicitation) { + if (!elicitation.enabled()) { + return McpToolResult.builder() + .error(true) + .addTextContent("Elicitation is not supported by the connected client") + .build(); + } + + String schema = "{\"type\":\"object\",\"properties\":{\"email\":{\"type\":\"string\"}}}"; + + try { + McpElicitationResponse response = elicitation.request(req -> req + .message("Please provide your email address.") + .schema(schema) + .timeout(Duration.ofSeconds(30))); + + if (response.action() != McpElicitationAction.ACCEPT) { + return McpToolResult.create("User did not provide the requested input."); + } + + String email = response.content() + .map(content -> content.get("email").asString().orElse("unknown")) + .orElse("unknown"); + return McpToolResult.create("Captured email: " + email); + } catch (McpElicitationException e) { + return McpToolResult.builder() + .error(true) + .addTextContent(e.getMessage()) + .build(); + } } ``` @@ -502,20 +671,27 @@ Below is an example of a tool that uses the Sampling feature. `McpSampling` obje ```java @Mcp.Tool("Uses MCP Sampling to ask the connected client model.") -List samplingTool(McpSampling sampling) { +McpToolResult samplingTool(McpSampling sampling) { if (!sampling.enabled()) { - throw new McpToolErrorException("This tool requires sampling feature"); + return McpToolResult.builder() + .error(true) + .addTextContent("This tool requires sampling feature") + .build(); } try { - var message = McpSamplingMessages.textContent("Write a 3-line summary of Helidon MCP Sampling.", McpRole.USER); McpSamplingResponse response = sampling.request(req -> req .timeout(Duration.ofSeconds(10)) .systemPrompt("You are a concise, helpful assistant.") - .addMessage(message)); - return List.of(McpToolContents.textContent(response.asTextMessage())); + .addTextMessage(McpSamplingTextMessage.builder().text("Write a 3-line summary of Helidon MCP Sampling.").role(McpRole.USER).build())); + return McpToolResult.builder() + .addTextContent(response.asTextMessage().text()) + .build(); } catch (McpSamplingException e) { - throw new McpToolErrorException(e.getMessage()); + return McpToolResult.builder() + .error(true) + .addTextContent(e.getMessage()) + .build(); } } ``` @@ -530,15 +706,18 @@ Below is an example of a tool that uses the Roots feature. `McpRoots` object can ```java @Mcp.Tool("Request MCP Roots to the connected client.") -List rootsTool(McpRoots mcpRoots) { +McpToolResult rootsTool(McpRoots mcpRoots) { if (!mcpRoots.enabled()) { - throw new McpToolErrorException("Roots are not supported by the client"); + return McpToolResult.builder() + .error(true) + .addTextContent("Roots are not supported by the client") + .build(); } List roots = mcpRoots.listRoots(); McpRoot root = roots.getFirst(); URI uri = root.uri(); String name = root.name().orElse("Unknown"); - return List.of(McpToolContents.textContent("Server updated roots")); + return McpToolResult.create("Server updated roots"); } ``` diff --git a/docs/mcp-declarative/migration_guide_1.1.md b/docs/mcp-declarative/migration_guide_1.1.md new file mode 100644 index 00000000..85de66d5 --- /dev/null +++ b/docs/mcp-declarative/migration_guide_1.1.md @@ -0,0 +1,101 @@ +# Migration Guide for Version 1.1.0 + +Helidon MCP `1.1.0` provides support for MCP specification `2025-06-18`. This document +summarizes the key changes and explains how to migrate existing code to `1.1.0`. + +## Overview of Changes + +The Helidon MCP Declarative API remains largely unchanged; however, supported method signatures have changed. +The sections below describe how to migrate MCP components to version `1.1.0`. + +## Tools + +The `McpToolContent` class is removed, along with the `McpToolContents` factory class. + +Previous API: +```java +@Mcp.Tool("description") +List tool() { + return List.of(McpToolContents.textContent("text")); +} +``` + +Updated API: +```java +@Mcp.Tool("description") +McpToolResult tool() { + return McpToolResult.create("text"); +} +``` + +For details about the `McpToolResult` class, see [README.md](README.md#tool-result) and +[Migration Guide](../mcp/migration_guide_1.1.md#migrate-from-mcptoolcontents). + +## Prompts + +The `McpPromptContent` class is removed, along with the `McpPromptContents` factory class. + +Previous API: +```java +@Mcp.Prompt("description") +List prompt() { + return List.of(McpPromptContents.textContent("text")); +} +``` + +Updated API: +```java +@Mcp.Prompt("description") +McpPromptResult prompt() { + return McpPromptResult.create("text"); +} +``` + +For details about the `McpPromptResult` class, see [README.md](README.md#prompt-content-types) and +[Migration Guide](../mcp/migration_guide_1.1.md#migrate-from-mcppromptcontents). + +## Resources + +The `McpResourceContent` class is removed, along with the `McpResourceContents` factory class. + +Previous API: +```java +@Mcp.Resource(uri = "http://resource", description = "description") +List resource() { + return List.of(McpResourceContents.textContent("text")); +} +``` + +Updated API: +```java +@Mcp.Resource(uri = "http://resource", description = "description") +McpResourceResult resource() { + return McpResourceResult.create("text"); +} +``` + +For details about the `McpResourceResult` class, see [README.md](README.md#resource-content-types) and +[Migration Guide](../mcp/migration_guide_1.1.md#migrate-from-mcpresourcecontents). + +## Completions + +The `McpCompletionContent` class is removed, along with the `McpCompletionContents` factory class. + +Previous API: +```java +@Mcp.Completion("prompt") +McpCompletionContent completion() { + return McpCompletionContents.completion("suggestion"); +} +``` + +Updated API: +```java +@Mcp.Completion("prompt") +McpCompletionResult completion() { + return McpCompletionResult.create("suggestion"); +} +``` + +For details about the `McpCompletionResult` class, see [README.md](README.md#completion-content-type) and +[Migration Guide](../mcp/migration_guide_1.1.md#migrate-mcpcompletion-interface). \ No newline at end of file diff --git a/docs/mcp/README.md b/docs/mcp/README.md index 111fd05e..9f05c55f 100644 --- a/docs/mcp/README.md +++ b/docs/mcp/README.md @@ -1,14 +1,13 @@ # Helidon MCP Extension -Helidon support for the Model Context Protocol (MCP). +Server-side Helidon support for the Model Context Protocol (MCP). ## Overview -The Model Context Protocol (MCP) defines a standard communication method that enables LLMs (Large Language Models) to interact -with both internal and external data sources. More than just a protocol, MCP establishes a connected environment of AI agents -capable of accessing real-time information. MCP follows a client-server architecture: clients, typically used by AI agents, -initiate communication, while servers manage access to data sources and provide data retrieval capabilities. Helidon offers -server-side support and can be accessed by any client that implements the MCP specification. +The Model Context Protocol (MCP) defines a standardized way for LLMs (Large Language Models) to interact with internal and +external data sources. MCP uses a client-server architecture in which clients (typically AI agents) initiate communication and +servers expose capabilities for data access, retrieval, and interaction. Helidon provides MCP server-side support that can be +consumed by any client implementing the MCP specification. ## Maven Coordinates @@ -23,16 +22,16 @@ To create your first MCP server using Helidon, add the following dependency to y ## Usage -This section walks you through creating and configuring various MCP components. +This section describes how to create and configure core MCP components in Helidon. ### MCP Server -Servers provide the fundamental building blocks for adding context to language models via MCP. It is accessible through a -configurable HTTP endpoint. The server manages client connections and provides features described in the following sections. -The MCP server is implemented as a Helidon `HttpFeature` and registered with the web server's routing. To host multiple servers, -simply register additional `McpServerFeature` instances with unique paths. Each path acts as a unique entry point for MCP clients. -Use the`McpServerFeature` builder to register MCP components. The server's name and version are shared with clients during -connection initialization, but Helidon imposes no constraints on how you manage them. +Servers provide the primary integration point for adding context to language models through MCP. A server is exposed through a +configurable HTTP endpoint, manages client connections, and provides the capabilities described in later sections. In Helidon, +an MCP server is implemented as an `HttpFeature` and registered in web server routing. To host multiple MCP servers, register +multiple `McpServerFeature` instances with distinct paths. Each path serves as a separate entry point for MCP clients. Use the +`McpServerFeature` builder to register tools, prompts, resources, and other MCP components. Server name and version are shared +with clients during initialization, and Helidon does not enforce naming or versioning conventions. **Example: Creating an MCP server** @@ -55,10 +54,11 @@ class McpServer { `Tools` enable models to interact with external systems: for example, by querying databases, calling APIs, or performing computations. To define a tool, provide a name, description, input schema, and business logic. Use the `addTool` method from the `McpServerFeature` builder to register it with the server. The name and description help LLMs understand its purpose. -The schema, written according to [JSON Schema Specification](https://json-schema.org/specification), defines the expected input format. The business logic is -implemented in the `process` method and uses `McpRequest` to access inputs. +The schema, written according to [JSON Schema Specification](https://json-schema.org/specification), defines the expected input +format. The business logic is implemented in the `tool` method and uses `McpToolRequest` to access inputs. The `McpToolRequest` +extends `McpRequest` and provides access to the `McpTool` instance via the `tool()` method. -#### Interface +#### Tool Interface Implement the `McpTool` interface to define a tool. @@ -74,29 +74,55 @@ class MyTool implements McpTool { return "Tool description"; } + @Override + public Optional title() { + return Optional.of("Tool Title"); + } + @Override public String schema() { + // Schema class is part of the Helidon JSON Schema API. + // For more details, see: https://helidon.io/docs/v4/se/json/schema return Schema.builder() .rootObject(root -> root - .addStringProperty("name", name -> name.description("Event name") - .required(true)) - .addIntegerProperty("productId", productId -> productId.description("The unique identifier for a product"))) + .addStringProperty("name", name -> name.description("Event name").required(true)) + .addIntegerProperty("productId", + productId -> productId.description("The unique identifier for a product"))) .build() .generate(); } @Override - public List process(McpRequest request) { - int productId = request.parameters() + public McpToolResult tool(McpToolRequest request) { + int productId = request.arguments() .get("productId") .asInteger() .orElse(0); - return List.of(McpToolContents.textContent("productId: " + productId)); + return McpToolResult.create("productId: " + productId); } } ``` -#### Builder +#### Tool request + +Client tool invocation request is accessible through `McpToolRequest`: + +- `name()` - Access the client tool name requested. +- `arguments()` - Access the client invocation arguments + +```java +@Override +public McpToolResult tool(McpToolRequest request) { + String name = request.name(); + int productId = request.arguments() + .get("productId") + .asInteger() + .orElse(0); + return McpToolResult.create("productId: " + productId); +} +``` + +#### Tool Builder You can also define a `Tool` directly within the server builder: @@ -109,28 +135,95 @@ class McpServer { .addTool(tool -> tool.name("name") .description("description") .schema("schema") - .tool(request -> McpToolContents.textContent("text")) + .tool(request -> McpToolResult.create("text")) .build()))); } } ``` -#### Tool Content Types +#### Structured content and output schema + +Structured content is returned as a JSON object in the `structuredContent` field of a result. For backwards compatibility, +a tool that returns structured content SHOULD also return the serialized JSON in a `TextContent` block. If there is no content +added to the `McpToolResult` builder, Helidon will serialize the structured content and add it by itself. +Tools have to provide an output schema for validation of structured results if it is using structured content. + +To add an output schema to the tool, implement the `outputSchema` method: +```java +@Override +public Optional outputSchema() { + // Schema.builder() is part of the Helidon JSON Schema API. + // For more details, see: https://helidon.io/docs/v4/se/json/schema + String schema = Schema.builder() + .rootObject(root -> root + .addStringProperty("name", name -> name.description("Event name") + .required(true))) + .build() + .generate(); + return Optional.of(schema); +} +``` + +or add it through the builder: +```java +McpServerFeature.builder() + .addTool(tool -> tool.name("name") + .description("description") + .title("Tool Title") + .schema("schema") + .outputSchema("outputSchema") + .tool(request -> McpToolResult.create("text")) + .build()); +``` + +#### Tool Result -Helidon supports four types of tool content: +Six types of tool result content can be created: - **Text**: Text content with the default `text/plain` media type. - **Image**: Image content with a custom media type. - **Audio**: Audio content with a custom media type. -- **Resource**: A reference to an `McpResource` via a URI (must be registered on the server). +- **Resource links**: A reference to a resource that does not have to be registered on the server. +- **Text Resource**: Text resource content with the default `text/plain` media type. +- **Binary Resource**: Binary resource content with a custom media type. + +Use the `McpToolResult` builder to create tool contents: + +```java +McpToolResult create() { + return McpToolResult.builder() + .addTextContent(text -> text.text("text")) + .addImageContent(image -> image.data(pngImageBytes()) + .mediaType(MediaTypes.create("image/png"))) + .addAudioContent(audio -> audio.data(wavAudioBytes()) + .mediaType(MediaTypes.create("audio/wav"))) + .addResourceLinkContent(link -> link.size(10) + .name("name") + .title("title") + .uri("https://foo") + .description("description") + .mediaType(MediaTypes.APPLICATION_JSON)) + .addTextResourceContent(resource -> resource.text("text") + .uri(URI.create("https://foo")) + .mediaType(MediaTypes.TEXT_PLAIN)) + .addBinaryResourceContent(resource -> resource.data(gzipBytes()) + .uri(URI.create("https://foo")) + .mediaType(MediaTypes.create("application/gzip"))) + .build(); +} +``` -Use the `McpToolContents` factory to create tool contents: +You can also use shortcut methods that accept only required parameters: ```java -McpToolContent text = McpToolContents.textContent("text"); -McpToolContent resource = McpToolContents.resourceContent("http://path"); -McpToolContent image = McpToolContents.imageContent(pngImageBytes(), MediaTypes.create("image/png")); -McpToolContent audio = McpToolContents.audioContent(wavAudioBytes(), MediaTypes.create("audio/wav")); +McpToolResult result = McpToolResult.builder() + .addTextContent("text") + .addImageContent(pngImageBytes(), MediaTypes.create("image/png")) + .addAudioContent(wavAudioBytes(), MediaTypes.create("audio/wav")) + .addResourceLinkContent("name", "https://foo") + .addTextResourceContent("text") + .addBinaryResourceContent(gzipBytes(), MediaTypes.create("application/gzip")) + .build(); ``` #### JSON Schema @@ -143,7 +236,8 @@ validation. Define it by returning a JSON string from the `schema()` method. `Prompts` allow servers to provide structured messages and instructions for interacting with language models. They improve instruction quality and help LLMs generate better results. Each instruction is associated with a `Role` (either `assistant` or `user`) indicating who is providing the input. When calling a prompt, clients must supply argument values, which are defined with -names, descriptions, and whether they are required. Use the `McpPromptArgument` builder to define arguments. +names, descriptions, and whether they are required. Use the `McpPromptArgument` builder to define arguments. The `prompt` method +receives an `McpPromptRequest` which extends `McpRequest` and provides access to the `McpPrompt` instance via the `prompt()` method. #### Interface @@ -161,6 +255,11 @@ class MyPrompt implements McpPrompt { return "Prompt description"; } + @Override + public Optional title() { + return Optional.of("Prompt Title"); + } + @Override public List arguments() { return List.of(McpPromptArgument.builder() @@ -171,12 +270,31 @@ class MyPrompt implements McpPrompt { } @Override - public List prompt(McpRequest request) { - return List.of(McpPromptContents.textContent("text", McpRole.USER)); + public McpPromptResult prompt(McpPromptRequest request) { + return McpPromptResult.create("text"); } } ``` +#### Prompt request + +Client prompt invocation request is accessible through `McpPromptRequest`: + +- `name()` - Access the client prompt name requested. +- `arguments()` - Access the client invocation arguments + +```java +@Override +public McpPromptResult prompt(McpPromptRequest request) { + String name = request.name(); + int productId = request.arguments() + .get("productId") + .asInteger() + .orElse(0); + return McpPromptResult.create("productId: " + productId); +} +``` + #### Builder You can also create a `Prompt` directly via the builder: @@ -189,37 +307,68 @@ class McpServer { McpServerFeature.builder() .addPrompt(prompt -> prompt.name("name") .description("description") + .title("Prompt Title") .addArgument(argument -> argument.name("name") .description("Argument description") .required(true)) - .prompt(request -> McpPromptContents.textContent("text", Role.USER)) + .prompt(request -> McpPromptResult.create("text")) .build()))); } } ``` -#### Prompt Content Types +#### Prompt Result -Helidon supports four prompt content types: +Five prompt content types can be created: - **Text**: Text content with a default `text/plain` media type. - **Image**: Image content with a custom media type. - **Audio**: Audio content with a custom media type. -- **Resource**: URI references to `McpResource` instances. +- **Text Resource**: Text resource content with a default `text/plain` media type. +- **Binary Resource**: Binary resource content with a custom media type. -`Prompt` content can be created using `McpPromptContents` factory, and used as a result of the `Prompt` execution. +Create prompt content with the `McpPromptResult` builder: ```java -McpPromptContent text = McpPromptContents.textContent("text", Role.USER); -McpPromptContent resource = McpPromptContents.resourceContent("http://resource", Role.USER); -McpPromptContent image = McpPromptContents.imageContent(pngImageBytes(), MediaTypes.create("image/png"), Role.USER); -McpPromptContent audio = McpPromptContents.audioContent(wavAudioBytes(), MediaTypes.create("audio/wav"), Role.USER); +McpPromptResult create() { + return McpPromptResult.builder() + .addTextContent(text -> text.text("text") + .role(McpRole.ASSISTANT)) + .addImageContent(image -> image.data(pngImageBytes()) + .mediaType(MediaTypes.create("image/png")) + .role(McpRole.ASSISTANT)) + .addAudioContent(audio -> audio.data(wavAudioBytes()) + .mediaType(MediaTypes.create("audio/wav")) + .role(McpRole.ASSISTANT)) + .addTextResourceContent(resource -> resource.text("text") + .uri(URI.create("https://example.com")) + .mediaType(MediaTypes.create("text/plain")) + .role(McpRole.ASSISTANT)) + .addBinaryResourceContent(resource -> resource.data(pngImageBytes()) + .uri(URI.create("https://example.com")) + .mediaType(MediaTypes.create("text/plain")) + .role(McpRole.ASSISTANT)) + .build(); +} +``` + +You can also use shortcut methods that accept only required parameters: + +```java +McpPromptResult result = McpPromptResult.builder() + .addTextContent("text") + .addImageContent(pngImageBytes(), MediaTypes.create("image/png")) + .addAudioContent(wavAudioBytes(), MediaTypes.create("audio/wav")) + .addTextResourceContent("text") + .addBinaryResourceContent(gzipBytes(), MediaTypes.create("application/gzip")) + .build(); ``` ### Resources `Resources` allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Clients can list and read resources, which are defined by name, description, and media type. +The `read` method receives an `McpResourceRequest` which extends `McpRequest`. #### Interface @@ -242,18 +391,37 @@ class MyResource implements McpResource { return "Resource description"; } + @Override + public Optional title() { + return Optional.of("Resource title"); + } + @Override public MediaType mediaType() { return MediaTypes.TEXT_PLAIN; } @Override - public List read(McpRequest request) { - return List.of(McpResourceContents.textContent("text")); + public McpResourceResult read(McpResourceRequest request) { + return McpResourceResult.create(content); } } ``` +#### Resource request + +Client resource read request is accessible through `McpResourceRequest`: + +- `uri()` - Access the client resource `URI` requested. + +```java +@Override +public McpResourceResult read(McpResourceRequest request) { + URI uri = request.uri(); + return McpResourceResult.create(uri.toASCIIString()); +} +``` + #### Builder Define a resource in the builder using `addResource`. @@ -266,10 +434,10 @@ class McpServer { McpServerFeature.builder() .addResource(resource -> resource.name("MyResource") .uri("https://path") + .title("Resource Title") .description("Resource description") .mediaType(MediaTypes.TEXT_PLAIN) - .ressource(request -> McpResourceContents.textContent("text")) - .build()))); + .resource(request -> McpResourceResult.create("text"))))); } } ``` @@ -294,12 +462,17 @@ class MyResource implements McpResource { @Override public String name() { - return "MyResource"; + return "MyResourceTemplate"; } @Override public String description() { - return "Resource description"; + return "Resource template description"; + } + + @Override + public Optional title() { + return Optional.of("Resource template title"); } @Override @@ -308,12 +481,12 @@ class MyResource implements McpResource { } @Override - public List read(McpRequest request) { + public McpResourceResult read(McpResourceRequest request) { String path = request.parameters() .get("path") .asString() .orElse("Unknown"); - return List.of(McpResourceContents.textContent(path)); + return McpResourceResult.create(path); } } ``` @@ -331,31 +504,41 @@ class McpServer { .addResource(resource -> resource.name("MyResource") .uri("https://{path}") .description("Resource description") + .title("Resource Template Title") .mediaType(MediaTypes.TEXT_PLAIN) - .ressource(request -> { - String path = request.parameters() - .get("path") - .asString() - .orElse("Unknown"); - return McpResourceContents.textContent(path); - }) + .resource(request -> request.parameters().get("path").asString().map(McpResourceResult::create).get()) .build()))); } } ``` -#### Resource Content Types +#### Resource Result -Helidon supports two resource content types: +Two resource content types can be created: - **Text**: Text content with `text/plain` media type. - **Binary**: Binary content with custom media type content. -`Resource` content can be created using `McpResourceContents` factory: +Create resource content with the `McpResourceResult` builder: ```java -McpResourceContent text = McpResourceContents.textContent("text"); -McpResourceContent binary = McpResourceContents.binaryContent(gzipBytes(), "application/gzip"); +McpResourceResult create() { + return McpResourceResult.builder() + .addTextContent(text -> text.text("text") + .mediaType(MediaTypes.TEXT_PLAIN)) + .addBinaryContent(binary -> binary.data(gzipBytes()) + .mediaType(MediaTypes.create("application/gzip"))) + .build(); +} +``` + +You can also use shortcut methods: + +```java +McpResourceResult text = McpResourceResult.create("text"); +McpResourceResult binary = McpResourceResult.builder() + .addBinaryContent(gzipBytes(), MediaTypes.create("application/gzip")) + .build(); ``` ### Resource Subscribers @@ -365,16 +548,21 @@ If a client is no longer interested in receiving update notifications, it can is unsubscribe request. Generally, the MCP server processes subscribe and unsubscribe requests without -any user-provided code executed on the server side. Clients simply subscribe -and unsubscribe (within the same session) using the resource URI and updates +any user-provided code executed on the server side. Clients subscribe +and unsubscribe (within the same session) using the resource URI, and updates are propagated to all active subscribers in all sessions. -Helidon MCP supports server-side subscribers and unsubscribers in case logic needs -to be executed server side to handle those events. +Helidon MCP supports server-side subscribers and unsubscribers when custom logic must +be executed on the server to handle those events. #### Interface Implement the `McpResourceSubscriber` interface and register it via `addResourceSubscriber`. Interfaces for -subscribers and unsubscribers are identical, so we shall focus on subscribers in this section. +subscribers and unsubscribers are similar: + +- `McpResourceSubscriber` – server-side hook invoked when a client subscribes. The `subscribe` method receives an `McpSubscribeRequest`. +- `McpResourceUnsubscriber` – server-side hook invoked when a client unsubscribes. The `unsubscribe` method receives an `McpUnsubscribeRequest`. + +The following section focuses on subscribers. ```java class MyResourceSubscriber implements McpResourceSubscriber { @@ -391,24 +579,24 @@ class MyResourceSubscriber implements McpResourceSubscriber { } @Override - public Consumer subscribe() { - return request -> monitorResource(uri()); + public void subscribe(McpSubscribeRequest request) { + monitorResource(uri()); } } ``` -MCP subscriptions are available via the the features instance and can -send notifications manually as follows: +MCP subscriptions are available via the features instance and can +send notifications manually: + +The API surface for this feature is `McpSubscriptions`. ```java @Override -public Consumer subscribe() { - return request -> { - if (wasUpdated()) { - McpFeatures features = request.features(); - features.subscriptions().sendUpdate(uri()); - } - }; +public void subscribe(McpSubscribeRequest request) { + if (wasUpdated()) { + McpFeatures features = request.features(); + features.subscriptions().sendUpdate(uri()); + } } ``` @@ -432,8 +620,10 @@ class McpServer { ### Completion The `Completion` feature offers auto-suggestions for prompt arguments or resource template parameters, making the server easier -to use and explore. Each completion is bound to a prompt name or a URI template. Access arguments from `McpRequest`. -The completion's type, either prompt or resource template, is returned by the `referenceType` method. +to use and explore. Each completion is bound to a prompt name or a `URI` template. The `completion` method receives an +`McpCompletionRequest` which extends `McpRequest` and provides `name()`, `value()` and `context()` methods to get +the argument name, its current value, and previously resolved arguments. The completion's type, either prompt or +resource template, is returned by the `referenceType` method. #### Interface @@ -452,15 +642,25 @@ class MyCompletion implements McpCompletion { } @Override - public McpCompletionContent complete(McpRequest request) { - String name = request.parameters().get("name").asString().orElse("Unknown"); - String value = request.parameters().get("value").asString().orElse("Unknown"); - - return McpCompletionContents.completion("suggestion"); + public McpCompletionResult completion(McpCompletionRequest request) { + String name = request.name(); + String value = request.value(); + // Context when provided can contain previously resolved variables. + McpCompletionContext context = request.context().orElse(null); + Map arguments = context.arguments(); + return McpCompletionResult.create("suggestion"); } } ``` +#### Completion Context + +The `McpCompletionContext` provides additional information about the current completion request, specifically +the values of other arguments that have already been provided by the user. This allows for context-aware +suggestions (e.g., suggesting a city based on a previously selected country). + +- **`arguments()`**: Returns a `Map` of previously resolved completion arguments. + #### Builder Define completions in the server builder: @@ -470,36 +670,132 @@ class McpServer { public static void main(String[] args) { WebServer.builder() .routing(routing -> routing.addFeature( - McpHttpFeatureConfig.builder() + McpServerFeature.builder() .addCompletion(completion -> completion .reference("MyPrompt") - .completion(request -> McpCompletionContents.completion("suggestion")) + .completion(request -> McpCompletionResult.create("suggestion")) .build()))); } } ``` -#### Completion Content Type +#### Completion Result + +Create the completion result using the list of suggestion. + +```java +McpCompletionResult result = McpCompletionResult.create("suggestion1", "suggestion2"); +``` + +Use the builder for specific use cases where the total number of suggestions exceeds 100 items. + +```java +McpCompletionResult result = McpCompletionResult.builder() + .values("suggestion1", "suggestion2") + .total(3) + .hasMore(true) + .build(); +``` + +## McpRequest + +The `McpRequest` object is the base interface for every request, providing access to client-side data and features. + +- **`parameters()`**: Returns `McpParameters` for accessing client-provided parameters. +- **`meta()`**: Returns `McpParameters` for accessing client-provided metadata. +- **`features()`**: Returns `McpFeatures` for accessing advanced features such as logging, progress, cancellation, elicitation, sampling, and roots. +- **`protocolVersion()`**: Returns the negotiated protocol version between the server and the client. +- **`sessionContext()`**: Returns a `Context` for session-scoped data. +- **`requestContext()`**: Returns a `Context` for request-scoped data. + +```java +@Override +public McpToolResult tool(McpToolRequest request) { + String protocol = request.protocolVersion(); + // ... use protocol + return McpToolResult.create("Protocol version: " + protocol); +} +``` + +### Context Management -Only one type is supported — a list of suggestions: +The `McpRequest` provides access to two types of context: + +- **Session Context**: Used to store data that persists throughout the duration of the client's session. +- **Request Context**: Used to store data specific to the current request. + +This capability is useful for maintaining state between multiple tool calls or prompts within the same session. ```java -McpCompletionContent content = McpCompletionContents.completion("suggestion"); +@Override +public McpToolResult tool(McpToolRequest request) { + Context sessionContext = request.sessionContext(); + int callCount = sessionContext.get("callCount", Integer.class).orElse(0); + sessionContext.register("callCount", ++callCount); + return McpToolResult.create("This tool has been called " + callCount + " times in this session."); +} ``` ## MCP Parameters -Client parameters are available in `McpTool`, `McpPrompt`, and `McpCompletion` business logic via the `McpParameters` API: +Client parameters are available in `McpTool`, `McpPrompt`, and `McpCompletion` business logic via the `McpParameters` API. +This class provides a flexible way to access and convert parameters from the client request. + +### Basic Usage + +You can access parameters by their key and convert them to various types. ```java void process(McpRequest request) { McpParameters parameters = request.parameters(); - parameters.get("list").asList().get(); - parameters.get("age").asInteger().orElse(18); - parameters.get("authorized").asBoolean().orElse(false); - parameters.get("name").asString().orElse("defaultName"); - parameters.get("address").as(Address.class).orElseThrow(); + // Access nested parameters + McpParameters address = parameters.get("address"); + + // Convert to primitive types + String name = parameters.get("name").asString().orElse("defaultName"); + int age = parameters.get("age").asInteger().orElse(18); + boolean authorized = parameters.get("authorized").asBoolean().orElse(false); + + // Convert to a list of strings + List roles = parameters.get("roles").asList(String.class).orElse(List.of()); + + // Convert to a custom POJO + Address homeAddress = address.as(Address.class).orElseThrow(); +} +``` + +### Checking for Presence + +You can check if a parameter is present or empty. + +```java +void parameters(McpRequest request) { + McpParameters param = request.parameters().get("optionalParam"); + if (param.isPresent()) { + // ... + } + if (param.isEmpty()) { + // ... + } +} +``` + +### Advanced Conversions + +The `McpParameters` API also supports more advanced conversions. + +```java +void convert(McpToolRequest request) { + // Convert to a list of McpParameters to iterate over a JSON array + List items = request.parameters().get("items").asList().get(); + for (McpParameters item : items) { + String itemName = item.get("name").asString().get(); + double itemPrice = item.get("price").asDouble().get(); + } + + // Convert to a map + Map metadata = request.parameters().get("metadata").asMap().get(); } ``` @@ -532,7 +828,7 @@ class LoggingTool implements McpTool { } @Override - public List process(McpRequest request) { + public McpToolResult tool(McpToolRequest request) { McpLogger logger = request.features().logger(); logger.info("Logging data"); @@ -543,7 +839,7 @@ class LoggingTool implements McpTool { logger.critical("Logging data"); logger.alert("Logging data"); - return List.of(McpToolContents.textContent("text")); + return McpToolResult.create("text"); } } ``` @@ -572,14 +868,14 @@ class ProgressTool implements McpTool { } @Override - public List process(McpRequest request) { + public McpToolResult tool(McpToolRequest request) { McpProgress progress = request.features().progress(); progress.total(100); for (int i = 1; i <= 10; i++) { longRunningTask(); progress.send(i * 10); } - return List.of(McpToolContents.textContent("text")); + return McpToolResult.create("text"); } } ``` @@ -607,6 +903,7 @@ McpServerFeature.builder() .promptsPageSize(1) .resourcesPageSize(1) .resourceTemplatesPageSize(1) + .build(); ``` ### Cancellation @@ -615,6 +912,11 @@ The MCP Cancellation feature enables verification of whether a client has issued typically made when a process is taking an extended amount of time, and the client opts not to wait for the completion of the operation. Cancellation status can be accessed from the `McpFeatures` class. +The API returns a `McpCancellationResult` which contains: + +- `isRequested()` – whether cancellation was requested +- `reason()` – cancellation reason provided by the client + #### Example Example of a Tool checking for cancellation request. @@ -637,11 +939,7 @@ private class CancellationTool implements McpTool { } @Override - public Function> tool() { - return this::process; - } - - private List process(McpRequest request) { + public McpToolResult tool(McpToolRequest request) { long now = System.currentTimeMillis(); long timeout = now + TimeUnit.SECONDS.toMillis(5); McpCancellation cancellation = request.features().cancellation(); @@ -649,12 +947,99 @@ private class CancellationTool implements McpTool { while (now < timeout) { if (cancellation.verify().isRequested()) { String reason = cancellation.verify().reason(); - return List.of(McpToolContents.textContent(reason)); + return McpToolResult.create(reason); } longRunningOperation(); now = System.currentTimeMillis(); } - return List.of(McpToolContents.textContent("text")); + return McpToolResult.create("text"); + } +} +``` + +### Elicitation + +The `Elicitation` feature allows a server to request additional structured user input through the connected MCP client during +request processing. This capability is useful when execution requires runtime information that is not available in the initial tool or prompt +arguments (for example, confirmation data, credentials, or user-selected options). + +Elicitation support is optional on the client side. Always verify support before sending a request: + +```java +McpElicitation elicitation = request.features().elicitation(); +if (!elicitation.enabled()) { +} +``` + +An elicitation request includes: + +- `message` – prompt shown to the user by the client +- `schema` – JSON Schema describing the expected response payload +- `timeout` – optional timeout (defaults to 5 minutes) + +Client responses include: + +- `action()` – `ACCEPT`, `DECLINE`, or `CANCEL` +- `content()` – response payload as `McpParameters` (present when accepted) + +#### Example + +```java +class ElicitationTool implements McpTool { + @Override + public String name() { + return "elicitation-tool"; + } + + @Override + public String description() { + return "Collects additional user input using MCP elicitation."; + } + + @Override + public String schema() { + return ""; + } + + @Override + public McpToolResult tool(McpToolRequest request) { + McpElicitation elicitation = request.features().elicitation(); + if (!elicitation.enabled()) { + return McpToolResult.builder() + .error(true) + .addTextContent("Elicitation is not supported by the connected client") + .build(); + } + + String userSchema = """ + { + "type": "object", + "properties": { + "email": {"type": "string"} + } + } + """; + + try { + McpElicitationResponse response = elicitation.request(req -> req + .message("Please provide your email address.") + .schema(userSchema) + .timeout(Duration.ofSeconds(30))); + + if (response.action() != McpElicitationAction.ACCEPT) { + return McpToolResult.create("User did not provide the requested input."); + } + + String email = response.content() + .map(content -> content.get("email").asString().orElse("unknown")) + .orElse("unknown"); + return McpToolResult.create("Captured email: " + email); + } catch (McpElicitationException e) { + return McpToolResult.builder() + .error(true) + .addTextContent(e.getMessage()) + .build(); + } } } ``` @@ -688,35 +1073,54 @@ McpSamplingRequest request = McpSamplingRequest.builder() .timeout(Duration.ofSeconds(10)) .stopSequences(List.of("stop1")) .includeContext(McpIncludeContext.NONE) - .addMessage(McpSamplingMessages.textContent("text", McpRole.USER)) + .addTextMessage(McpSamplingTextMessage.builder() + .text("text") + .role(McpRole.USER) + .build()) .build(); ``` -Once your request is built, send it using the sampling feature. The request method may throw an `McpSamplingException` if an -error occurs during processing. On success, it returns an McpSamplingResponse containing the response message, the model used, -and optionally a stop reason. +Once your request is built, send it using the sampling feature. + +#### Sampling data model + +Three types of sampling request messages can be created: + +- **Text**: Text message content. +- **Image**: Image message content with a custom media type. +- **Audio**: Audio message content with a custom media type. + +Create sampling request messages with the `McpSamplingRequest` builder: ```java -try { - McpSamplingResponse response = sampling.request(req -> req.addMessage(message)); -} catch(McpSamplingException exception) { - // Manage error -} +McpSamplingRequest request = McpSamplingRequest.builder() + .addTextMessage(message -> message.text("Explain Helidon MCP in one paragraph.") + .role(McpRole.USER)) + .addImageMessage(image -> image.data(pngBytes) + .mediaType(MediaTypes.create("image/png")) + .role(McpRole.USER)) + .addAudioMessage(audio -> audio.data(wavBytes) + .mediaType(MediaTypes.create("audio/wav")) + .role(McpRole.USER)) + .build(); ``` -The messages you send are prompts to the language model, and they follow the same structure as MCP prompts. You can use the -`McpSamplingMessages` utility class to create different types of messages for the client model: +Once your request is built, send it using the sampling feature. The `request` method may throw an `McpSamplingException` if an +error occurs during processing. On success, it returns an `McpSamplingResponse` containing the response message, the model used, +and optionally a stop reason. + +Sampling responses may include a `McpStopReason` (for example `END_TURN`, `STOP_SEQUENCE`, or `MAX_TOKENS`). ```java -var text = McpSamplingMessages.textContent("Explain Helidon MCP in one paragraph.", McpRole.USER); -var image = McpSamplingMessages.imageContent(pngBytes, MediaTypes.create("image/png"), McpRole.USER); -var audio = McpSamplingMessages.audioContent(wavBytes, MediaTypes.create("audio/wav"), McpRole.USER); +try { + McpSamplingResponse response = sampling.request(req -> req.addTextMessage("text")); +} catch (McpSamplingException exception) { + // Handle error +} ``` - #### Example -Below is an example of a tool that uses the Sampling feature. If the connected client does not support sampling, the tool -throws a `McpToolErrorException`. +Below is an example of a tool that uses the Sampling feature. ```java class SamplingTool implements McpTool { @@ -736,25 +1140,28 @@ class SamplingTool implements McpTool { } @Override - public Function> tool() { - return request -> { - var sampling = request.features().sampling(); - - if (!sampling.enabled()) { - throw new McpToolErrorException("This tool requires sampling feature"); - } + public McpToolResult tool(McpToolRequest request) { + var sampling = request.features().sampling(); + + if (!sampling.enabled()) { + return McpToolResult.builder() + .error(true) + .addTextContent("This tool requires sampling feature") + .build(); + } - try { - var message = McpSamplingMessages.textContent("Write a 3-line summary of Helidon MCP Sampling.", McpRole.USER); - McpSamplingResponse response = sampling.request(req -> req - .timeout(Duration.ofSeconds(10)) - .systemPrompt("You are a concise, helpful assistant.") - .addMessage(message)); - return List.of(McpToolContents.textContent(response.asTextMessage())); - } catch (McpSamplingException e) { - throw new McpToolErrorException(e.getMessage()); - } - }; + try { + McpSamplingResponse response = sampling.request(req -> req + .timeout(Duration.ofSeconds(10)) + .systemPrompt("You are a concise, helpful assistant.") + .addTextMessage("Write a 3-line summary of Helidon MCP Sampling.")); + return McpToolResult.create(response.asTextMessage().text()); + } catch (McpSamplingException e) { + return McpToolResult.builder() + .error(true) + .addTextContent(e.getMessage()) + .build(); + } } } ``` @@ -765,6 +1172,8 @@ Roots establish the boundaries within the filesystem that define where servers a directories and files a server can access. Servers can request the current list of roots from compatible clients and receive notifications whenever that list is updated. +If a roots-related operation fails, Helidon may throw `McpRootException`. + #### Example ```java @@ -776,7 +1185,7 @@ class RootNameTool implements McpTool { @Override public String description() { - return "Get the list of roots available"; + return "Retrieve the list of available roots"; } @Override @@ -785,18 +1194,19 @@ class RootNameTool implements McpTool { } @Override - public Function> tool() { - return request -> { - McpRoots mcpRoots = request.features().roots(); - if (!mcpRoots.enabled()) { - throw new McpToolErrorException("Roots are not supported by the client"); - } - List roots = mcpRoots.listRoots(); - McpRoot root = roots.getFirst(); - URI uri = root.uri(); - String name = root.name().orElse("Unknown"); - return List.of(McpToolContents.textContent("Server updated roots")); - }; + public McpToolResult tool(McpToolRequest request) { + McpRoots mcpRoots = request.features().roots(); + if (!mcpRoots.enabled()) { + return McpToolResult.builder() + .addTextContent("Roots are not supported by the client") + .error(true) + .build(); + } + List roots = mcpRoots.listRoots(); + McpRoot root = roots.getFirst(); + URI uri = root.uri(); + String name = root.name().orElse("Unknown"); + return McpToolResult.create("Server updated roots"); } } ``` @@ -821,9 +1231,9 @@ class McpServer { Config config = Config.create(); WebServer.builder() - .routing(routing -> routing.addFeature( - McpHttpFeatureConfig.builder() - .config(config.get("mcp.server")))); + .routing(routing -> routing.addFeature(McpServerFeature.builder() + .config(config.get("mcp.server")) + .build())); } } ``` diff --git a/docs/mcp/migration_guide_1.1.md b/docs/mcp/migration_guide_1.1.md new file mode 100644 index 00000000..c540586e --- /dev/null +++ b/docs/mcp/migration_guide_1.1.md @@ -0,0 +1,555 @@ +# Migration Guide for Version 1.1.0 + +Helidon MCP `1.1.0` provides support for MCP specification `2025-06-18`. This document +summarizes the key changes and explains how to migrate existing code to `1.1.0`. + +## Overview of Changes + +- When MCP components used to return a `List`, they now return `Mcp*Result`. +- MCP component signatures based on `Function>` are replaced with typed methods such as `Mcp*Result method(Mcp*Request request)`. +- Content is now created directly on result builders and no longer requires factory classes. +- Typed request objects provide direct access to request-specific data, without parsing all values through `McpParameters`. +- Result types follow the same pattern and provide more customization options. + +The sections below describe how to migrate MCP components to version `1.1.0`. + +## Tools + +### Migrate McpTool interface + +Previous API: +```java +@Override +public Function> tool() { + return request -> List.of(McpToolContents.textContent("text")); +} +``` + +Updated API: +```java +@Override +public McpToolResult tool(McpToolRequest request) { + return McpToolResult.create("text"); +} +``` + +For details about the `McpToolResult` builder, see [Tool Result](README.md#tool-result-builder-and-content-types). + +### Migrate McpRequest to McpToolRequest + +The `McpToolRequest` type extends `McpRequest`. To access tool arguments provided by the client, +use `McpToolRequest#arguments()`. + +Previous API: +```java +@Override +public Function> tool() { + return request -> { + String input = request.parameters().get("input").asString().orElse(""); + return List.of(); + }; +} +``` + +Updated API: +```java +@Override +public McpToolResult tool(McpToolRequest request) { + String input = request.arguments().get("input").asString().orElse(""); + return McpToolResult.create("text"); +} +``` + +The `parameters()` method remains available and exposes the full JSON-RPC parameter payload from the client. + +### Migrate from McpToolContents + +The `McpToolContents` factory is removed in favor of builders. `McpToolContent` types are now created with dedicated builders. + +- McpToolTextContent + +Previous API: +```java +McpToolContent text = McpToolContents.textContent("text"); +``` + +Updated API: +```java +McpToolTextContent content = McpToolTextContent.builder().text("text").build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpToolResult result = McpToolResult.builder() + .addTextContent("text") + .build(); +//This is the equivalent of +McpToolResult result = McpToolResult.create("text"); +``` + +- McpToolImageContent + +Previous API: +```java +McpToolContent image = McpToolContents.imageContent("text".getBytes(), MediaTypes.TEXT_PLAIN); +``` + +Updated API: +```java +McpToolImageContent content = McpToolImageContent.builder() + .data("text".getBytes()) + .mediaType(MediaTypes.TEXT_PLAIN) + .build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpToolResult result = McpToolResult.builder() + .addImageContent("text".getBytes(), MediaTypes.TEXT_PLAIN) + .build(); +``` +Or +```java +McpToolResult result = McpToolResult.builder() + .addImageContent(image -> image.data("text".getBytes()) + .mediaType(MediaTypes.TEXT_PLAIN)) + .build(); +``` + +- McpToolAudioContent + +Previous API: +```java +McpToolContent audio = McpToolContents.audioContent("audio".getBytes(), MediaTypes.TEXT_PLAIN); +``` + +Updated API: +```java +McpToolAudioContent content = McpToolAudioContent.builder() + .data("text".getBytes()) + .mediaType(MediaTypes.TEXT_PLAIN) + .build(); +``` + +Alternatively, create `McpToolAudioContent` directly in the result builder: +```java +McpToolResult result = McpToolResult.builder() + .addAudioContent("audio".getBytes(), MediaTypes.TEXT_PLAIN) + .build(); +``` +Or +```java +McpToolResult result = McpToolResult.builder() + .addAudioContent(audio -> audio.data("audio".getBytes()) + .mediaType(MediaTypes.TEXT_PLAIN)) + .build(); +``` + +- McpToolTextResourceContent + +Previous API: +```java +McpResourceContent content = McpResourceContents.textContent("text"); +McpToolContent resource = McpToolContents.resourceContent(URI.create("https://foo"), content); +``` + +Updated API: +```java +McpToolTextResourceContent content = McpToolTextResourceContent.builder() + .uri(URI.create("http://resource")) + .text("resource") + .mediaType(MediaTypes.TEXT_PLAIN) + .build(); +McpToolResult result = McpToolResult.builder() + .addTextResourceContent(content) + .build(); +``` + +Alternatively, create `McpToolTextResourceContent` directly in the result builder: +```java +McpToolResult result = McpToolResult.builder() + .addTextResourceContent(resource -> resource + .uri(URI.create("http://resource")) + .text("resource") + .mediaType(MediaTypes.TEXT_PLAIN)) + .build(); +``` + +- McpToolBinaryResourceContent + +Previous API: +```java +McpResourceContent content = McpResourceContents.binaryContent("binary".getBytes(), MediaTypes.APPLICATION_JSON); +McpToolContent resource = McpToolContents.resourceContent(URI.create("https://foo"), content); +``` + +Updated API: +```java +McpToolBinaryResourceContent content = McpToolBinaryResourceContent.builder() + .data("binary".getBytes()) + .uri(URI.create("http://localhost:8080")) + .mediaType(MediaTypes.APPLICATION_OCTET_STREAM) + .build(); +McpToolResult result = McpToolResult.builder() + .addBinaryResourceContent(content) + .build(); +``` + +Alternatively, create `McpToolTextResourceContent` directly in the result builder: +```java +McpToolResult result = McpToolResult.builder() + .addBinaryResourceContent(resource -> resource + .data("resource") + .uri(URI.create("http://resource")) + .mediaType(MediaTypes.TEXT_PLAIN)) + .build(); +``` + +This approach is more flexible and supports a wider range of use cases. + +### Migrate from McpToolErrorException + +This exception was previously used to indicate tool errors. This is no longer required; +set the tool error flag on the result builder instead. + +Previous API: +```java +throw new McpToolErrorException("exception text"); +``` + +Updated API: +```java +return McpToolResult.builder() + .error(true) + .addTextContent("exception text") + .build(); +``` + +## Prompts + +### Migrate McpPrompt interface + +Previous API: +```java +@Override +public Function> prompt() { + return request -> List.of(McpPromptContents.textContent("text")); +} +``` + +Updated API: +```java +@Override +public McpPromptResult prompt(McpPromptRequest request) { + return McpPromptResult.create("text"); +} +``` + +For details about the `McpPromptResult` builder, see [Prompt Result](README.md#prompt-result-builder-and-content-types). + +### Migrate McpRequest to McpPromptRequest + +The `McpPromptRequest` extends `McpRequest`. To access prompt arguments provided by the client, +use `McpPromptRequest#arguments()`. + +Previous API: +```java +@Override +public Function> prompt() { + return request -> { + String input = request.parameters().get("input").asString().orElse(""); + return List.of(); + }; +} +``` + +Updated API: +```java +@Override +public McpPromptResult prompt(McpPromptRequest request) { + String input = request.arguments().get("input").asString().orElse(""); + return McpPromptResult.create("text"); +} +``` + +### Migrate from McpPromptContents + +The `McpPromptContents` factory is removed in favor of builders. `McpPromptContent` types are now created with dedicated builders. + +- McpPromptTextContent + +Previous API: +```java +McpPromptContent text = McpPromptContents.textContent("text"); +``` + +Updated API: +```java +McpPromptTextContent content = McpPromptTextContent.builder() + .text("text") + .role(McpRole.USER) + .build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpPromptResult result = McpPromptResult.builder() + .addTextContent("text") + .build(); +//This is the equivalent of +McpPromptResult result = McpPromptResult.create("text"); +``` + +- McpPromptImageContent + +Previous API: +```java +McpPromptContent image = McpPromptContents.imageContent("binary".getBytes(), MediaTypes.TEXT_PLAIN); +``` + +Updated API: +```java +McpPromptImageContent content = McpPromptImageContent.builder() + .data("binary".getBytes()) + .mediaType(MediaTypes.TEXT_PLAIN) + .role(McpRole.USER) + .build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpPromptResult result = McpPromptResult.builder() + .addImageContent("binary".getBytes(), MediaTypes.TEXT_PLAIN) + .build(); +``` + +- McpPromptAudioContent + +Previous API: +```java +McpPromptContent audio = McpPromptContents.audioContent("audio".getBytes(), MediaTypes.TEXT_PLAIN); +``` + +Updated API: +```java +McpPromptAudioContent content = McpPromptAudioContent.builder() + .data("audio".getBytes()) + .mediaType(MediaTypes.TEXT_PLAIN) + .role(McpRole.USER) + .build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpPromptResult result = McpPromptResult.builder() + .addAudioContent("audio".getBytes(), MediaTypes.TEXT_PLAIN) + .build(); +``` + +- McpPromptTextResourceContent + +Previous API: +```java +McpResourceContent content = McpResourceContents.textContent("text"); +McpPromptContent resource = McpPromptContents.resourceContent(URI.create("https://foo"), content); +``` + +Updated API: +```java +McpPromptTextResourceContent content = McpPromptTextResourceContent.builder() + .uri(URI.create("https://resource")) + .text("text") + .mediaType(MediaTypes.TEXT_PLAIN) + .build(); +McpPromptResult result = McpPromptResult.builder() + .addTextResourceContent(content) + .build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpPromptResult result = McpPromptResult.builder() + .addTextResourceContent(resource -> resource + .uri(URI.create("https://resource")) + .text("text") + .mediaType(MediaTypes.TEXT_PLAIN)) + .build(); +``` + +- McpPromptBinaryResourceContent + +Previous API: +```java +McpResourceContent content = McpResourceContents.binaryContent("binary".getBytes(), MediaTypes.APPLICATION_JSON); +McpPromptContent resource = McpPromptContents.resourceContent(URI.create("https://foo"), content); +``` + +Updated API: +```java +McpPromptBinaryResourceContent content = McpPromptBinaryResourceContent.builder() + .data("binary".getBytes()) + .uri(URI.create("http://localhost:8080")) + .mediaType(MediaTypes.APPLICATION_OCTET_STREAM) + .build(); +McpPromptResult result = McpPromptResult.builder() + .addBinaryResourceContent(content) + .build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpPromptResult result = McpPromptResult.builder() + .addBinaryResourceContent(resource -> resource + .data("resource") + .uri(URI.create("http://resource")) + .mediaType(MediaTypes.TEXT_PLAIN)) + .build(); +``` + +## Resources + +### Migrate McpResource interface + +Previous API: +```java +@Override +public Function> resource() { + return request -> List.of(McpResourceContents.textContent("text")); +} +``` + +Updated API: +```java +@Override +public McpResourceResult resource(McpResourceRequest request) { + return McpResourceResult.create("text"); +} +``` + +For details about the `McpResourceResult` builder, see [Resource Result](README.md#resource-result-builder-and-content-types). + +### Migrate McpRequest to McpResourceRequest + +The `McpResourceRequest` extends `McpRequest`. To access the URI being read, use the `McpResourceRequest#uri()` method. + +Previous API: +```java +@Override +public Function> resource() { + return request -> { + URI uri = URI.create(request.parameters().get("uri").asString().orElse("")); + return List.of(); + }; +} +``` + +Updated API: +```java +@Override +public McpResourceResult resource(McpResourceRequest request) { + URI uri = request.uri(); + return McpResourceResult.create("text"); +} +``` + +### Migrate from McpResourceContents + +The `McpResourceContents` factory is removed in favor of builders. The `McpResourceContent` types can be +created using their builders. + +- McpResourceTextContent + +Previous API: +```java +McpResourceContent text = McpResourceContents.textContent("text"); +``` + +Updated API: +```java +McpResourceTextContent content = McpResourceTextContent.builder() + .text("text") + .mediaType(MediaTypes.TEXT_PLAIN) + .build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpResourceResult result = McpResourceResult.builder() + .addTextContent("text") + .build(); +//This is the equivalent of +McpResourceResult result = McpResourceResult.create("text"); +``` + +- McpResourceBinaryContent + +Previous API: +```java +McpResourceContent binary = McpResourceContents.binaryContent("binary".getBytes(), MediaTypes.APPLICATION_JSON); +``` + +Updated API: +```java +McpResourceBinaryContent content = McpResourceBinaryContent.builder() + .data("binary".getBytes()) + .mediaType(MediaTypes.APPLICATION_JSON) + .build(); +``` + +Alternatively, create it directly in the result builder: +```java +McpResourceResult result = McpResourceResult.builder() + .addBinaryContent("binary".getBytes(), MediaTypes.APPLICATION_JSON) + .build(); +``` + +## Completion + +### Migrate McpCompletion interface + +Previous API: +```java +@Override +public Function completion() { + return request -> McpCompletionContents.completion("suggestion1", "suggestion2"); +} +``` + +Updated API: +```java +@Override +public McpCompletionResult completion(McpCompletionRequest request) { + return McpCompletionResult.create("suggestion1", "suggestion2"); +} +``` + +For details about the `McpCompletionResult` builder, see [Completion Result](README.md#completion-result-builder). + +### Migrate McpRequest to McpCompletionRequest + +The `McpCompletionRequest` extends `McpRequest`. To access completion details, use the methods: +- `McpCompletionRequest#name()`: name of the argument or resource template being completed +- `McpCompletionRequest#value()`: current value of the field being completed +- `McpCompletionRequest#context()`: additional context (e.g., other arguments already filled) + +Previous API: +```java +@Override +public Function completion() { + return request -> { + String name = request.parameters().get("name").asString().orElse(""); + String value = request.parameters().get("value").asString().orElse(""); + return McpCompletionContents.completion("suggestion1", "suggestion2"); + }; +} +``` + +Updated API: +```java +@Override +public McpCompletionResult completion(McpCompletionRequest request) { + String name = request.name(); + String value = request.value(); + return McpCompletionResult.create("suggestion1", "suggestion2"); +} +```