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
Expand Up @@ -45,6 +45,11 @@
*/
String name() default ELEMENT_NAME;

/**
* A human-readable name for this prompt.
*/
String title() default "";

/**
* @return An optional description.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
*/
String name() default ELEMENT_NAME;

/**
* @return A human-readable title for the tool.
*/
String title() default "";

/**
* @return A human-readable description of the tool. A hint to the model.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.micronaut.http.HttpRequest;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.ProtocolVersions;
import jakarta.inject.Singleton;

import java.util.HashMap;
Expand All @@ -31,26 +32,24 @@
@Internal
@Singleton
final class DefaultMcpTransportContextExtractor implements McpTransportContextExtractor<HttpRequest<?>> {
public static final String HTTP_HEADER_MCP_PROTOCOL_VERSION = "MCP-Protocol-Version";
public static final String DEFAULT_PROTOCOL_VERSION = "2025-03-26";
public static final String HTTP_HEADER_MCP_SESSION_ID = "Mcp-Session-Id";
public static final String HTTP_HEADER_DEFAULT_LAST_EVENT_ID = "Last-Event-ID";

@Override
public McpTransportContext extract(HttpRequest<?> request) {
return McpTransportContext.create(metadata(request));
}

private Map<String, Object> metadata(HttpRequest<?> request) {
HttpHeaders headers = request.getHeaders();
return metadata(request.getHeaders());
}

private Map<String, Object> metadata(HttpHeaders headers) {
Map<String, Object> metadata = new HashMap<>();
metadata.put(HTTP_HEADER_MCP_PROTOCOL_VERSION,
headers.get(HTTP_HEADER_MCP_PROTOCOL_VERSION, String.class)
.orElse(DEFAULT_PROTOCOL_VERSION));
headers.get(HTTP_HEADER_MCP_SESSION_ID, String.class)
.ifPresent(v -> metadata.put(HTTP_HEADER_MCP_SESSION_ID, v));
headers.get(HTTP_HEADER_DEFAULT_LAST_EVENT_ID, String.class)
.ifPresent(v -> metadata.put(HTTP_HEADER_DEFAULT_LAST_EVENT_ID, v));
metadata.put(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION,
headers.get(io.modelcontextprotocol.spec.HttpHeaders.PROTOCOL_VERSION, String.class)
.orElse(ProtocolVersions.MCP_2025_03_26));
headers.get(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID, String.class)
.ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.MCP_SESSION_ID, v));
headers.get(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID, String.class)
.ifPresent(v -> metadata.put(io.modelcontextprotocol.spec.HttpHeaders.LAST_EVENT_ID, v));
return metadata;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2017-2025 original authors
*
* 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
*
* https://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.micronaut.mcp.server;

import io.micronaut.core.annotation.Internal;

/**
* An MCP Server.
*/
@Internal
public interface McpHttpServer extends AutoCloseable {
/**
* Starts the MCP Server.
*/
void start();

/**
*
* @return The Port the server is running at
*/
int getPort();

/**
*
* @return the MCP endpoint path. For example, `/mcp`
*/
String getEndpoint();

/**
* Returns the default {@link McpHttpServer}.
* @return The default {@link McpHttpServer}
* @throws IllegalStateException If no {@link McpHttpServer} implementation exists on
* the classpath.
*/
static McpHttpServer getDefault() {
return McpHttpServerInternal.getDefaultMapper();
}

/**
* Creates a new default {@link McpHttpServer}.
* @return The default {@link McpHttpServer}
* @throws IllegalStateException If no {@link McpHttpServer} implementation exists on
* the classpath.
*/
static McpHttpServer createDefault() {
return McpHttpServerInternal.createDefaultMapper();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2017-2025 original authors
*
* 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
*
* https://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.micronaut.mcp.server;

import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import io.micronaut.core.annotation.Internal;

/**
* Utility class for creating a default {@link McpHttpServer} instance.
* This class provides a single method to create a default mapper using the {@link ServiceLoader}
* mechanism.
*/
@Internal
final class McpHttpServerInternal {
private static McpHttpServer defaultJsonMapper = null;

/**
* Returns the cached default {@link McpHttpServer} instance.
* If the default mapper has not been created yet, it will be initialized using the
* {@link #createDefaultMapper()} method.
* @return the default {@link McpHttpServer} instance
* @throws IllegalStateException if no default {@link McpHttpServer} implementation is
* found
*/
static McpHttpServer getDefaultMapper() {
if (defaultJsonMapper == null) {
defaultJsonMapper = McpHttpServerInternal.createDefaultMapper();
}
return defaultJsonMapper;
}

/**
* Creates a default {@link McpHttpServer} instance using the {@link ServiceLoader} mechanism.
* The default mapper is resolved by loading the first available
* {@link McpHttpServerSupplier} implementation on the classpath.
* @return the default {@link McpHttpServer} instance
* @throws IllegalStateException if no default {@link McpHttpServer} implementation is
* found
*/
static McpHttpServer createDefaultMapper() {
AtomicReference<IllegalStateException> ex = new AtomicReference<>();
return ServiceLoader.load(McpHttpServerSupplier.class).stream().flatMap(p -> {
try {
McpHttpServerSupplier supplier = p.get();
return Stream.ofNullable(supplier);
} catch (Exception e) {
addException(ex, e);
return Stream.empty();
}
}).flatMap(jsonMapperSupplier -> {
try {
return Stream.ofNullable(jsonMapperSupplier.get());
} catch (Exception e) {
addException(ex, e);
return Stream.empty();
}
}).findFirst().orElseThrow(() -> {
if (ex.get() != null) {
return ex.get();
} else {
return new IllegalStateException("No default McpHttpServer implementation found");
}
});
}

private static void addException(AtomicReference<IllegalStateException> ref, Exception toAdd) {
ref.updateAndGet(existing -> {
if (existing == null) {
return new IllegalStateException("Failed to initialize default McpHttpServer", toAdd);
} else {
existing.addSuppressed(toAdd);
return existing;
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2017-2025 original authors
*
* 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
*
* https://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.micronaut.mcp.server;

import io.micronaut.core.annotation.Internal;

import java.util.function.Supplier;

/**
* Strategy interface for resolving a {@link McpHttpServer}.
*/
@Internal
public interface McpHttpServerSupplier extends Supplier<McpHttpServer> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.ResourceCapabilities.class,
io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.ToolCapabilities.class,
io.modelcontextprotocol.spec.McpSchema.Implementation.class,
io.modelcontextprotocol.spec.McpSchema.Role.class,
io.modelcontextprotocol.spec.McpSchema.Annotations.class,
io.modelcontextprotocol.spec.McpSchema.Resource.class,
io.modelcontextprotocol.spec.McpSchema.ResourceTemplate.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
public final class PromptRegistry
extends AbstractMcpMethodRegistry<McpServerFeatures.SyncPromptSpecification, McpServerFeatures.AsyncPromptSpecification, McpStatelessServerFeatures.SyncPromptSpecification, McpStatelessServerFeatures.AsyncPromptSpecification> {
public static final String MEMBER_NAME = "name";
public static final String MEMBER_TITLE = "title";
public static final String MEMBER_DESCRIPTION = "description";
private final BeanContext beanContext;

Expand Down Expand Up @@ -134,6 +135,9 @@ private <B> McpSchema.GetPromptResult promptResult(BeanDefinition<B> beanDefinit
}
Object result = method.invoke(bean, args);

if (result instanceof McpSchema.GetPromptResult promptResult) {
return promptResult;
}
if (method.getReturnType().getType().isAssignableFrom(String.class)) {
McpSchema.TextContent assistantContent = new McpSchema.TextContent(result.toString());
McpSchema.PromptMessage assistantMessage = new McpSchema.PromptMessage(McpSchema.Role.ASSISTANT, assistantContent);
Expand All @@ -146,7 +150,7 @@ private <B> McpSchema.GetPromptResult promptResult(BeanDefinition<B> beanDefinit
}

private McpSchema.Prompt prompt(BeanDefinition<?> beanDefinition, ExecutableMethod<?, ?> method) {
return new McpSchema.Prompt(promptName(method), promptDescription(method).orElse(null),
return new McpSchema.Prompt(promptName(method), promptTitle(method).orElse(null), promptDescription(method).orElse(null),
promptArguments(beanDefinition, method));
}

Expand Down Expand Up @@ -189,6 +193,10 @@ private Optional<String> promptArgumentDescription(Argument<?> argument) {
.flatMap(ann -> ann.stringValue(MEMBER_DESCRIPTION));
}

private static Optional<String> promptTitle(ExecutableMethod<?, ?> method) {
return method.stringValue(Prompt.class, MEMBER_TITLE);
}

private static Optional<String> promptDescription(ExecutableMethod<?, ?> method) {
return method.stringValue(Prompt.class, MEMBER_DESCRIPTION);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public final class ToolRegistry extends AbstractMcpMethodRegistry<McpServerFeatu
* @see <a href="https://json-schema.org/understanding-json-schema/reference/type">JSON Schema Type</a>
*/
private static final String MEMBER_DESCRIPTION = "description";
private static final String MEMBER_TITLE = "title";
private static final String KEY_TYPE = "type";

private final JsonSchemaClassPathResourceLoader jsonSchemaClassPathResourceLoader;
Expand Down Expand Up @@ -243,6 +244,7 @@ private static <T extends Exception> McpError mapException(McpErrorExceptionMapp
private <B> McpSchema.Tool tool(ExecutableMethod<B, Object> method) {
McpSchema.Tool.Builder toolBuilder = McpSchema.Tool.builder()
.name(toolName(method));
toolTitle(method).ifPresent(toolBuilder::title);
toolDescription(method).ifPresent(toolBuilder::description);
Optional<String> jsonSchemaOptional = jsonSchema(method);
if (jsonSchemaOptional.isPresent()) {
Expand Down Expand Up @@ -319,6 +321,10 @@ private static List<String> toolArgumentsNames(ExecutableMethod<?, ?> method) {
return names;
}

private static Optional<String> toolTitle(ExecutableMethod<?, ?> method) {
return method.stringValue(Tool.class, MEMBER_TITLE);
}

private static Optional<String> toolDescription(ExecutableMethod<?, ?> method) {
return method.stringValue(Tool.class, MEMBER_DESCRIPTION);
}
Expand Down
4 changes: 4 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ include 'micronaut-mcp-client-langchain4j'
include 'micronaut-mcp-bom'
include 'test-suite-graal'
include 'test-suite-jackson-databind'
include 'test-suite-mcp-http-tck'
include 'test-suite-mcp-http-tck-common'
include 'test-suite-mcp-http-tck-sync'
include 'test-suite-mcp-http-tck-async'

enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS'

Expand Down
26 changes: 26 additions & 0 deletions test-suite-mcp-http-tck-async/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
`java-library`
}
dependencies {
testAnnotationProcessor(mnSerde.micronaut.serde.processor)
testImplementation(mnSerde.micronaut.serde.jackson)
testImplementation(mn.micronaut.http.server.netty)
testAnnotationProcessor(mnJsonSchema.micronaut.json.schema.processor)
testImplementation(mnJsonSchema.micronaut.json.schema.annotations)

testAnnotationProcessor(mn.micronaut.inject.java)
testRuntimeOnly(mnLogging.logback.classic)

testImplementation(projects.testSuiteMcpHttpTckCommon)
testImplementation(projects.testSuiteMcpHttpTck)
testImplementation(mnTest.junit.platform.suite.api)
// Add JUnit Jupiter API and engines
testImplementation(mnTest.junit.jupiter.api)
testRuntimeOnly(mnTest.junit.jupiter.engine)
// Add JUnit Platform Suite engine to run @Suite tests
testRuntimeOnly(libs.junit.platform.engine)
testImplementation("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test> {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.modelcontextprotocol.server.http.tck.async;

import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;

@SelectPackages({
"io.modelcontextprotocol.server.http.tck"
})
@Suite
@SuiteDisplayName("MCP HTTP Server TCK for Micronaut HTTP Server Async")
public class HttpServerSyncSuite {
}
Loading
Loading