From 7cb31803748130297cc4acb99fad1ec74cda4390 Mon Sep 17 00:00:00 2001
From: Scott Lewis
Date: Tue, 13 May 2025 12:15:00 -0700
Subject: [PATCH 1/8] Suggested annotation classes for mcp java sdk
to address feature request:
https://github.com/modelcontextprotocol/java-sdk/issues/224
---
.../io/modelcontextprotocol/annotations/Tool.java | 15 +++++++++++++++
.../annotations/ToolParam.java | 9 +++++++++
.../util/ToolCallResultConverter.java | 9 +++++++++
3 files changed, 33 insertions(+)
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/annotations/Tool.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/annotations/ToolParam.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/ToolCallResultConverter.java
diff --git a/mcp/src/main/java/io/modelcontextprotocol/annotations/Tool.java b/mcp/src/main/java/io/modelcontextprotocol/annotations/Tool.java
new file mode 100644
index 000000000..b2e7e8f8b
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/annotations/Tool.java
@@ -0,0 +1,15 @@
+package io.modelcontextprotocol.annotations;
+
+import io.modelcontextprotocol.util.ToolCallResultConverter;
+
+public @interface Tool {
+
+ String name() default "";
+
+ String description() default "";
+
+ boolean returnDirect() default false;
+
+ Class extends ToolCallResultConverter> resultConverter();
+
+}
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/annotations/ToolParam.java b/mcp/src/main/java/io/modelcontextprotocol/annotations/ToolParam.java
new file mode 100644
index 000000000..83667cf97
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/annotations/ToolParam.java
@@ -0,0 +1,9 @@
+package io.modelcontextprotocol.annotations;
+
+public @interface ToolParam {
+
+ boolean required() default true;
+
+ String description() default "";
+
+}
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/ToolCallResultConverter.java b/mcp/src/main/java/io/modelcontextprotocol/util/ToolCallResultConverter.java
new file mode 100644
index 000000000..d58981026
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/util/ToolCallResultConverter.java
@@ -0,0 +1,9 @@
+package io.modelcontextprotocol.util;
+
+import java.lang.reflect.Type;
+
+public interface ToolCallResultConverter {
+
+ String convert(Object result, Type returnType);
+
+}
\ No newline at end of file
From a06e4263caf7724970967902c260299c674161cf Mon Sep 17 00:00:00 2001
From: Scott Lewis
Date: Mon, 19 May 2025 18:26:36 -0700
Subject: [PATCH 2/8] Update to suggested annotation additions. Simplified
Tool, ToolParam annotation classes. Added ToolResult, ToolAnnotation, and
ToolAnnotations annotations. Added ToolGroupService interface and
supporting tools.utils.*Description classes, to allow runtime querying of an
service interface class' ToolDescriptions (one for each method with Tool
annotation at runtime).
---
.../annotations/Tool.java | 15 -------
.../annotations/ToolParam.java | 9 ----
.../tools/annotation/Tool.java | 18 ++++++++
.../tools/annotation/ToolAnnotation.java | 28 ++++++++++++
.../tools/annotation/ToolAnnotations.java | 12 ++++++
.../tools/annotation/ToolParam.java | 18 ++++++++
.../tools/annotation/ToolResult.java | 14 ++++++
.../tools/annotation/package-info.java | 1 +
.../tools/service/ToolGroupService.java | 12 ++++++
.../tools/service/package-info.java | 1 +
.../tools/util/ToolAnnotationDescription.java | 28 ++++++++++++
.../tools/util/ToolDescription.java | 43 +++++++++++++++++++
.../tools/util/ToolParamDescription.java | 28 ++++++++++++
.../tools/util/ToolResultDescription.java | 14 ++++++
.../tools/util/package-info.java | 1 +
15 files changed, 218 insertions(+), 24 deletions(-)
delete mode 100644 mcp/src/main/java/io/modelcontextprotocol/annotations/Tool.java
delete mode 100644 mcp/src/main/java/io/modelcontextprotocol/annotations/ToolParam.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotation.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolParam.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolResult.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/package-info.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/service/ToolGroupService.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/service/package-info.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolResultDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/package-info.java
diff --git a/mcp/src/main/java/io/modelcontextprotocol/annotations/Tool.java b/mcp/src/main/java/io/modelcontextprotocol/annotations/Tool.java
deleted file mode 100644
index b2e7e8f8b..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/annotations/Tool.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package io.modelcontextprotocol.annotations;
-
-import io.modelcontextprotocol.util.ToolCallResultConverter;
-
-public @interface Tool {
-
- String name() default "";
-
- String description() default "";
-
- boolean returnDirect() default false;
-
- Class extends ToolCallResultConverter> resultConverter();
-
-}
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/annotations/ToolParam.java b/mcp/src/main/java/io/modelcontextprotocol/annotations/ToolParam.java
deleted file mode 100644
index 83667cf97..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/annotations/ToolParam.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package io.modelcontextprotocol.annotations;
-
-public @interface ToolParam {
-
- boolean required() default true;
-
- String description() default "";
-
-}
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
new file mode 100644
index 000000000..662ae1b1c
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
@@ -0,0 +1,18 @@
+package io.modelcontextprotocol.tools.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Tool {
+
+ String name() default "";
+
+ String description() default "";
+
+ ToolAnnotation[] annotations() default {};
+
+}
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotation.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotation.java
new file mode 100644
index 000000000..e2045c962
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotation.java
@@ -0,0 +1,28 @@
+package io.modelcontextprotocol.tools.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Supports the addition of ToolAnnotations to Tool spec in the MCP schema
+ * (draft as of 5/18/2025) located here
+ */
+@Repeatable(ToolAnnotations.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface ToolAnnotation {
+ boolean destructiveHint() default false;
+
+ boolean idempotentHint() default false;
+
+ boolean openWorldHint() default false;
+
+ boolean readOnlyHint() default false;
+
+ String title() default "";
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java
new file mode 100644
index 000000000..3a1c9e192
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java
@@ -0,0 +1,12 @@
+package io.modelcontextprotocol.tools.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface ToolAnnotations {
+ ToolAnnotation[] value() default {};
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolParam.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolParam.java
new file mode 100644
index 000000000..0801956ad
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolParam.java
@@ -0,0 +1,18 @@
+package io.modelcontextprotocol.tools.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface ToolParam {
+
+ String name() default "";
+
+ String description() default "";
+
+ boolean required() default true;
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolResult.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolResult.java
new file mode 100644
index 000000000..8765fa44d
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolResult.java
@@ -0,0 +1,14 @@
+package io.modelcontextprotocol.tools.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface ToolResult {
+
+ String description() default "";
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/package-info.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/package-info.java
new file mode 100644
index 000000000..ee01eb56b
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/package-info.java
@@ -0,0 +1 @@
+package io.modelcontextprotocol.tools.annotation;
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/service/ToolGroupService.java b/mcp/src/main/java/io/modelcontextprotocol/tools/service/ToolGroupService.java
new file mode 100644
index 000000000..7c7c3e660
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/service/ToolGroupService.java
@@ -0,0 +1,12 @@
+package io.modelcontextprotocol.tools.service;
+
+import java.util.List;
+
+import io.modelcontextprotocol.tools.util.ToolDescription;
+
+public interface ToolGroupService {
+
+ default List getToolDescriptions(String interfaceClassName) {
+ return ToolDescription.fromService(this, interfaceClassName);
+ }
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/service/package-info.java b/mcp/src/main/java/io/modelcontextprotocol/tools/service/package-info.java
new file mode 100644
index 000000000..4515b402e
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/service/package-info.java
@@ -0,0 +1 @@
+package io.modelcontextprotocol.tools.service;
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationDescription.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationDescription.java
new file mode 100644
index 000000000..d33e9406f
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationDescription.java
@@ -0,0 +1,28 @@
+package io.modelcontextprotocol.tools.util;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import io.modelcontextprotocol.tools.annotation.ToolAnnotation;
+
+/**
+ * Describes the ToolAnnotation type in the MCP schema (draft as of 5/18/2025)
+ * located here
+ */
+public record ToolAnnotationDescription(boolean destructiveHint, boolean idempotentHint, boolean openWorldHint,
+ boolean readOnlyHint, String title) {
+
+ public static List fromAnnotations(ToolAnnotation[] annotations) {
+ if (annotations != null) {
+ return Arrays.asList(annotations).stream().map(a -> {
+ return new ToolAnnotationDescription(a.destructiveHint(), a.idempotentHint(), a.openWorldHint(),
+ a.readOnlyHint(), a.title());
+ }).collect(Collectors.toList());
+ } else {
+ return Collections.emptyList();
+ }
+ }
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java
new file mode 100644
index 000000000..98206b398
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java
@@ -0,0 +1,43 @@
+package io.modelcontextprotocol.tools.util;
+
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import io.modelcontextprotocol.tools.annotation.Tool;
+import io.modelcontextprotocol.tools.annotation.ToolAnnotations;
+
+public record ToolDescription(String name, String description, List toolParamDescriptions,
+ ToolResultDescription resultDescriptions, List toolAnnotationDescriptions) {
+
+ public static List fromClass(Class> clazz) {
+ return Arrays.asList(clazz.getMethods()).stream().map(m -> {
+ // skip static methods
+ if (!Modifier.isStatic(m.getModifiers())) {
+ // Look for Tool annotation
+ Tool ma = m.getAnnotation(Tool.class);
+ if (ma != null) {
+ // Look for ToolAnnotations method annotation
+ ToolAnnotations tas = m.getAnnotation(ToolAnnotations.class);
+ return new ToolDescription(m.getName(), ma.description(),
+ ToolParamDescription.fromParameters(m.getParameters()), ToolResultDescription.fromMethod(m),
+ ToolAnnotationDescription.fromAnnotations(tas != null ? tas.value() : null));
+ }
+ }
+ return null;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+
+ }
+
+ public static List fromService(Object svc, String serviceClass) {
+ Optional> optClass = Arrays.asList(svc.getClass().getInterfaces()).stream().filter(c -> {
+ return c.getName().equals(serviceClass);
+ }).findFirst();
+ return optClass.isPresent() ? ToolDescription.fromClass(optClass.get()) : Collections.emptyList();
+ }
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java
new file mode 100644
index 000000000..8eed894f5
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java
@@ -0,0 +1,28 @@
+package io.modelcontextprotocol.tools.util;
+
+import java.lang.reflect.Parameter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import io.modelcontextprotocol.tools.annotation.ToolParam;
+
+public record ToolParamDescription(String name, String description, boolean required) {
+
+ public static List fromParameters(Parameter[] parameters) {
+ return parameters != null ? Arrays.asList(parameters).stream().map(p -> {
+ ToolParam tp = p.getAnnotation(ToolParam.class);
+ if (tp != null) {
+ String name = tp.name();
+ if ("".equals(name)) {
+ name = p.getName();
+ }
+ return new ToolParamDescription(name, tp.description(), tp.required());
+ }
+ return null;
+ }).filter(Objects::nonNull).collect(Collectors.toList()) : Collections.emptyList();
+ }
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolResultDescription.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolResultDescription.java
new file mode 100644
index 000000000..c8e3d4176
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolResultDescription.java
@@ -0,0 +1,14 @@
+package io.modelcontextprotocol.tools.util;
+
+import java.lang.reflect.Method;
+
+import io.modelcontextprotocol.tools.annotation.ToolResult;
+
+public record ToolResultDescription(String description, Class> returnType) {
+
+ public static ToolResultDescription fromMethod(Method method) {
+ ToolResult tr = method.getAnnotation(ToolResult.class);
+ return tr != null ? new ToolResultDescription(tr.description(), method.getReturnType())
+ : new ToolResultDescription("", method.getReturnType());
+ }
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/package-info.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/package-info.java
new file mode 100644
index 000000000..3fa8f5590
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/util/package-info.java
@@ -0,0 +1 @@
+package io.modelcontextprotocol.tools.util;
\ No newline at end of file
From 7a6e2f3165d487a3c132f6c5af0c7f072e939692 Mon Sep 17 00:00:00 2001
From: Scott Lewis
Date: Mon, 19 May 2025 18:29:51 -0700
Subject: [PATCH 3/8] Removed ToolCallResultConverter interface
---
.../util/ToolCallResultConverter.java | 9 ---------
1 file changed, 9 deletions(-)
delete mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/ToolCallResultConverter.java
diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/ToolCallResultConverter.java b/mcp/src/main/java/io/modelcontextprotocol/util/ToolCallResultConverter.java
deleted file mode 100644
index d58981026..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/util/ToolCallResultConverter.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package io.modelcontextprotocol.util;
-
-import java.lang.reflect.Type;
-
-public interface ToolCallResultConverter {
-
- String convert(Object result, Type returnType);
-
-}
\ No newline at end of file
From d65fbabd838436c8b5e23074c4d98e12f7b963f3 Mon Sep 17 00:00:00 2001
From: Scott Lewis
Date: Tue, 27 May 2025 09:59:45 -0700
Subject: [PATCH 4/8] Simplification of ToolAnnotations to match python
ToolAnnotations model.
---
.../tools/annotation/ToolAnnotations.java | 17 ++++++++++-
.../tools/util/ToolAnnotationDescription.java | 28 -------------------
.../util/ToolAnnotationsDescription.java | 21 ++++++++++++++
.../tools/util/ToolDescription.java | 4 +--
.../tools/util/ToolParamDescription.java | 4 +--
5 files changed, 41 insertions(+), 33 deletions(-)
delete mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationsDescription.java
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java
index 3a1c9e192..b30bdb360 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java
@@ -5,8 +5,23 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+/**
+ * Supports the addition of ToolAnnotations to Tool spec in the MCP schema
+ * (draft as of 5/18/2025) located here
+ */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ToolAnnotations {
- ToolAnnotation[] value() default {};
+
+ String title() default "";
+
+ boolean destructiveHint() default false;
+
+ boolean idempotentHint() default false;
+
+ boolean openWorldHint() default false;
+
+ boolean readOnlyHint() default false;
+
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationDescription.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationDescription.java
deleted file mode 100644
index d33e9406f..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationDescription.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package io.modelcontextprotocol.tools.util;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import io.modelcontextprotocol.tools.annotation.ToolAnnotation;
-
-/**
- * Describes the ToolAnnotation type in the MCP schema (draft as of 5/18/2025)
- * located here
- */
-public record ToolAnnotationDescription(boolean destructiveHint, boolean idempotentHint, boolean openWorldHint,
- boolean readOnlyHint, String title) {
-
- public static List fromAnnotations(ToolAnnotation[] annotations) {
- if (annotations != null) {
- return Arrays.asList(annotations).stream().map(a -> {
- return new ToolAnnotationDescription(a.destructiveHint(), a.idempotentHint(), a.openWorldHint(),
- a.readOnlyHint(), a.title());
- }).collect(Collectors.toList());
- } else {
- return Collections.emptyList();
- }
- }
-}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationsDescription.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationsDescription.java
new file mode 100644
index 000000000..1a67d24f5
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationsDescription.java
@@ -0,0 +1,21 @@
+package io.modelcontextprotocol.tools.util;
+
+import io.modelcontextprotocol.tools.annotation.ToolAnnotations;
+
+/**
+ * Describes the ToolAnnotations type in the MCP schema (draft as of 5/18/2025)
+ * located here
+ */
+public record ToolAnnotationsDescription(boolean destructiveHint, boolean idempotentHint, boolean openWorldHint,
+ boolean readOnlyHint, String title) {
+
+ public static ToolAnnotationsDescription fromAnnotations(ToolAnnotations annotations) {
+ if (annotations != null) {
+ return new ToolAnnotationsDescription(annotations.destructiveHint(), annotations.idempotentHint(),
+ annotations.openWorldHint(), annotations.readOnlyHint(), annotations.title());
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java
index 98206b398..b210b2f41 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java
@@ -12,7 +12,7 @@
import io.modelcontextprotocol.tools.annotation.ToolAnnotations;
public record ToolDescription(String name, String description, List toolParamDescriptions,
- ToolResultDescription resultDescriptions, List toolAnnotationDescriptions) {
+ ToolResultDescription resultDescription, ToolAnnotationsDescription toolAnnotationsDescription) {
public static List fromClass(Class> clazz) {
return Arrays.asList(clazz.getMethods()).stream().map(m -> {
@@ -25,7 +25,7 @@ public static List fromClass(Class> clazz) {
ToolAnnotations tas = m.getAnnotation(ToolAnnotations.class);
return new ToolDescription(m.getName(), ma.description(),
ToolParamDescription.fromParameters(m.getParameters()), ToolResultDescription.fromMethod(m),
- ToolAnnotationDescription.fromAnnotations(tas != null ? tas.value() : null));
+ ToolAnnotationsDescription.fromAnnotations(tas));
}
}
return null;
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java
index 8eed894f5..c68f133f1 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java
@@ -9,7 +9,7 @@
import io.modelcontextprotocol.tools.annotation.ToolParam;
-public record ToolParamDescription(String name, String description, boolean required) {
+public record ToolParamDescription(String name, String description, boolean required, Parameter parameter) {
public static List fromParameters(Parameter[] parameters) {
return parameters != null ? Arrays.asList(parameters).stream().map(p -> {
@@ -19,7 +19,7 @@ public static List fromParameters(Parameter[] parameters)
if ("".equals(name)) {
name = p.getName();
}
- return new ToolParamDescription(name, tp.description(), tp.required());
+ return new ToolParamDescription(name, tp.description(), tp.required(), p);
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList()) : Collections.emptyList();
From 08053aca3ee0a76a10d4cc581518ef44b976b1e9 Mon Sep 17 00:00:00 2001
From: Scott Lewis
Date: Tue, 27 May 2025 10:07:39 -0700
Subject: [PATCH 5/8] Removing ToolAnnotation class
---
.../tools/annotation/Tool.java | 2 +-
.../tools/annotation/ToolAnnotation.java | 28 -------------------
2 files changed, 1 insertion(+), 29 deletions(-)
delete mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotation.java
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
index 662ae1b1c..45a6491f7 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
@@ -13,6 +13,6 @@
String description() default "";
- ToolAnnotation[] annotations() default {};
+ ToolAnnotations annotations();
}
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotation.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotation.java
deleted file mode 100644
index e2045c962..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotation.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package io.modelcontextprotocol.tools.annotation;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Repeatable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Supports the addition of ToolAnnotations to Tool spec in the MCP schema
- * (draft as of 5/18/2025) located here
- */
-@Repeatable(ToolAnnotations.class)
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface ToolAnnotation {
- boolean destructiveHint() default false;
-
- boolean idempotentHint() default false;
-
- boolean openWorldHint() default false;
-
- boolean readOnlyHint() default false;
-
- String title() default "";
-
-}
From 28a00d2218c09222f01505e22435ff836ccdf5fc Mon Sep 17 00:00:00 2001
From: Scott Lewis
Date: Wed, 28 May 2025 12:56:59 -0700
Subject: [PATCH 6/8] Removed annotations from Tool annotation
---
.../java/io/modelcontextprotocol/tools/annotation/Tool.java | 2 --
1 file changed, 2 deletions(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
index 45a6491f7..dcc759545 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
@@ -13,6 +13,4 @@
String description() default "";
- ToolAnnotations annotations();
-
}
\ No newline at end of file
From 08cb1d8d654428d33cd5409cc733b71eba98c563 Mon Sep 17 00:00:00 2001
From: Scott Lewis
Date: Tue, 17 Jun 2025 17:24:08 -0700
Subject: [PATCH 7/8] Initial checkin of annotation classes for providing MCP
tools meta-data: @Tool, @ToolParam, @ToolResult, @ToolAnnotations along with
description (record) classes...e.g. ToolDescription for communicating the
meta-data across processes in a consistent way.
---
CODE_OF_CONDUCT.md | 119 ++
CONTRIBUTING.md | 94 ++
README.md | 7 +-
SECURITY.md | 21 +
mcp-bom/pom.xml | 2 +-
mcp-spring/mcp-spring-webflux/pom.xml | 12 +-
.../WebFluxSseIntegrationTests.java | 49 +-
mcp-spring/mcp-spring-webmvc/pom.xml | 12 +-
mcp-test/pom.xml | 4 +-
.../server/AbstractMcpAsyncServerTests.java | 2 +-
.../server/AbstractMcpSyncServerTests.java | 2 +-
mcp/pom.xml | 2 +-
.../client/McpAsyncClient.java | 4 +-
.../client/McpSyncClient.java | 8 +
.../server/McpAsyncServer.java | 1043 ++++++++---------
.../server/McpServer.java | 68 +-
.../spec/McpClientSession.java | 5 +-
.../spec/McpServerSession.java | 3 +-
.../tools/annotation/Tool.java | 16 +
.../tools/annotation/ToolAnnotations.java | 27 +
.../tools/annotation/ToolParam.java | 18 +
.../tools/annotation/ToolResult.java | 14 +
.../tools/annotation/package-info.java | 1 +
.../tools/service/ToolGroupService.java | 12 +
.../tools/service/package-info.java | 1 +
.../util/ToolAnnotationsDescription.java | 21 +
.../tools/util/ToolDescription.java | 43 +
.../tools/util/ToolParamDescription.java | 28 +
.../tools/util/ToolResultDescription.java | 14 +
.../tools/util/package-info.java | 1 +
.../DeafaultMcpUriTemplateManagerFactory.java | 23 +
.../util/DefaultMcpUriTemplateManager.java | 163 +++
.../util/McpUriTemplateManager.java | 52 +
.../util/McpUriTemplateManagerFactory.java | 22 +
.../McpUriTemplateManagerTests.java | 97 ++
.../client/StdioMcpAsyncClientTests.java | 4 +-
.../client/StdioMcpSyncClientTests.java | 4 +-
pom.xml | 17 +-
38 files changed, 1428 insertions(+), 607 deletions(-)
create mode 100644 CODE_OF_CONDUCT.md
create mode 100644 CONTRIBUTING.md
create mode 100644 SECURITY.md
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/Tool.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolAnnotations.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolParam.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/ToolResult.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/annotation/package-info.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/service/ToolGroupService.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/service/package-info.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolAnnotationsDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolParamDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/ToolResultDescription.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/tools/util/package-info.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java
create mode 100644 mcp/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..6009a645f
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,119 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our community a
+harassment-free experience for everyone, regardless of age, body size, visible or
+invisible disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming, diverse,
+inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our community
+include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes, and
+ learning from the experience
+- Focusing on what is best not just for us as individuals, but for the overall community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or advances of any kind
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email address, without
+ their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a professional
+ setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in response to
+any behavior that they deem inappropriate, threatening, offensive, or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject comments,
+commits, code, wiki edits, issues, and other contributions that are not aligned to this
+Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when an
+individual is officially representing the community in public spaces. Examples of
+representing our community include using an official e-mail address, posting via an
+official social media account, or acting as an appointed representative at an online or
+offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to
+the community leaders responsible for enforcement at mcp-coc@anthropic.com. All
+complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the reporter
+of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining the
+consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing clarity
+around the nature of the violation and an explanation of why the behavior was
+inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No interaction with
+the people involved, including unsolicited interaction with those enforcing the Code of
+Conduct, for a specified period of time. This includes avoiding interactions in community
+spaces as well as external channels like social media. Violating these terms may lead to
+a temporary or permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including sustained
+inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public communication
+with the community for a specified period of time. No public or private interaction with
+the people involved, including unsolicited interaction with those enforcing the Code of
+Conduct, is allowed during this period. Violating these terms may lead to a permanent
+ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community standards,
+including sustained inappropriate behavior, harassment of an individual, or aggression
+toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
+available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..517f32555
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,94 @@
+# Contributing to Model Context Protocol Java SDK
+
+Thank you for your interest in contributing to the Model Context Protocol Java SDK!
+This document outlines how to contribute to this project.
+
+## Prerequisites
+
+The following software is required to work on the codebase:
+
+- `Java 17` or above
+- `Docker`
+- `npx`
+
+## Getting Started
+
+1. Fork the repository
+2. Clone your fork:
+
+```bash
+git clone https://github.com/YOUR-USERNAME/java-sdk.git
+cd java-sdk
+```
+
+3. Build from source:
+
+```bash
+./mvnw clean install -DskipTests # skip the tests
+./mvnw test # run tests
+```
+
+## Reporting Issues
+
+Please create an issue in the repository if you discover a bug or would like to
+propose an enhancement. Bug reports should have a reproducer in the form of a code
+sample or a repository attached that the maintainers or contributors can work with to
+address the problem.
+
+## Making Changes
+
+1. Create a new branch:
+
+```bash
+git checkout -b feature/your-feature-name
+```
+
+2. Make your changes
+3. Validate your changes:
+
+```bash
+./mvnw clean test
+```
+
+### Change Proposal Guidelines
+
+#### Principles of MCP
+
+1. **Simple + Minimal**: It is much easier to add things to the codebase than it is to
+ remove them. To maintain simplicity, we keep a high bar for adding new concepts and
+ primitives as each addition requires maintenance and compatibility consideration.
+2. **Concrete**: Code changes need to be based on specific usage and implementation
+ challenges and not on speculative ideas. Most importantly, the SDK is meant to
+ implement the MCP specification.
+
+## Submitting Changes
+
+1. For non-trivial changes, please clarify with the maintainers in an issue whether
+ you can contribute the change and the desired scope of the change.
+2. For trivial changes (for example a couple of lines or documentation changes) there
+ is no need to open an issue first.
+3. Push your changes to your fork.
+4. Submit a pull request to the main repository.
+5. Follow the pull request template.
+6. Wait for review.
+7. For any follow-up work, please add new commits instead of force-pushing. This will
+ allow the reviewer to focus on incremental changes instead of having to restart the
+ review process.
+
+## Code of Conduct
+
+This project follows a Code of Conduct. Please review it in
+[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
+
+## Questions
+
+If you have questions, please create a discussion in the repository.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the MIT
+License.
+
+## Security
+
+Please review our [Security Policy](SECURITY.md) for reporting security issues.
\ No newline at end of file
diff --git a/README.md b/README.md
index 9fc17306e..0cd3f84a4 100644
--- a/README.md
+++ b/README.md
@@ -30,11 +30,8 @@ To run the tests you have to pre-install `Docker` and `npx`.
## Contributing
-Contributions are welcome! Please:
-
-1. Fork the repository
-2. Create a feature branch
-3. Submit a Pull Request
+Contributions are welcome!
+Please follow the [Contributing Guidelines](CONTRIBUTING.md).
## Team
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..74e9880fd
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+Thank you for helping us keep the SDKs and systems they interact with secure.
+
+## Reporting Security Issues
+
+This SDK is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model
+Context Protocol project.
+
+The security of our systems and user data is Anthropic’s top priority. We appreciate the
+work of security researchers acting in good faith in identifying and reporting potential
+vulnerabilities.
+
+Our security program is managed on HackerOne and we ask that any validated vulnerability
+in this functionality be reported through their
+[submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
+
+## Vulnerability Disclosure Program
+
+Our Vulnerability Program Guidelines are defined on our
+[HackerOne program page](https://hackerone.com/anthropic-vdp).
\ No newline at end of file
diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml
index 4f24f719f..7214dacda 100644
--- a/mcp-bom/pom.xml
+++ b/mcp-bom/pom.xml
@@ -7,7 +7,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOTmcp-bom
diff --git a/mcp-spring/mcp-spring-webflux/pom.xml b/mcp-spring/mcp-spring-webflux/pom.xml
index 63c32a8a8..a8b92bd09 100644
--- a/mcp-spring/mcp-spring-webflux/pom.xml
+++ b/mcp-spring/mcp-spring-webflux/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT../../pom.xmlmcp-spring-webflux
@@ -25,13 +25,13 @@
io.modelcontextprotocol.sdkmcp
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOTio.modelcontextprotocol.sdkmcp-test
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOTtest
@@ -82,6 +82,12 @@
${mockito.version}test
+
+ net.bytebuddy
+ byte-buddy
+ ${byte-buddy.version}
+ test
+ io.projectreactorreactor-test
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
index 660f814da..03fbc9962 100644
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
@@ -8,6 +8,8 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
@@ -651,9 +653,11 @@ void testInitialize(String clientType) {
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
- void testLoggingNotification(String clientType) {
+ void testLoggingNotification(String clientType) throws InterruptedException {
+ int expectedNotificationsCount = 3;
+ CountDownLatch latch = new CountDownLatch(expectedNotificationsCount);
// Create a list to store received logging notifications
- List receivedNotifications = new ArrayList<>();
+ List receivedNotifications = new CopyOnWriteArrayList<>();
var clientBuilder = clientBuilders.get(clientType);
@@ -709,6 +713,7 @@ void testLoggingNotification(String clientType) {
// Create client with logging notification handler
var mcpClient = clientBuilder.loggingConsumer(notification -> {
receivedNotifications.add(notification);
+ latch.countDown();
}).build()) {
// Initialize client
@@ -724,31 +729,28 @@ void testLoggingNotification(String clientType) {
assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed");
- // Wait for notifications to be processed
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(latch.await(5, TimeUnit.SECONDS)).as("Should receive notifications in reasonable time").isTrue();
- // Should have received 3 notifications (1 NOTICE and 2 ERROR)
- assertThat(receivedNotifications).hasSize(3);
+ // Should have received 3 notifications (1 NOTICE and 2 ERROR)
+ assertThat(receivedNotifications).hasSize(expectedNotificationsCount);
- Map notificationMap = receivedNotifications.stream()
- .collect(Collectors.toMap(n -> n.data(), n -> n));
+ Map notificationMap = receivedNotifications.stream()
+ .collect(Collectors.toMap(n -> n.data(), n -> n));
- // First notification should be NOTICE level
- assertThat(notificationMap.get("Notice message").level()).isEqualTo(McpSchema.LoggingLevel.NOTICE);
- assertThat(notificationMap.get("Notice message").logger()).isEqualTo("test-logger");
- assertThat(notificationMap.get("Notice message").data()).isEqualTo("Notice message");
+ // First notification should be NOTICE level
+ assertThat(notificationMap.get("Notice message").level()).isEqualTo(McpSchema.LoggingLevel.NOTICE);
+ assertThat(notificationMap.get("Notice message").logger()).isEqualTo("test-logger");
+ assertThat(notificationMap.get("Notice message").data()).isEqualTo("Notice message");
- // Second notification should be ERROR level
- assertThat(notificationMap.get("Error message").level()).isEqualTo(McpSchema.LoggingLevel.ERROR);
- assertThat(notificationMap.get("Error message").logger()).isEqualTo("test-logger");
- assertThat(notificationMap.get("Error message").data()).isEqualTo("Error message");
+ // Second notification should be ERROR level
+ assertThat(notificationMap.get("Error message").level()).isEqualTo(McpSchema.LoggingLevel.ERROR);
+ assertThat(notificationMap.get("Error message").logger()).isEqualTo("test-logger");
+ assertThat(notificationMap.get("Error message").data()).isEqualTo("Error message");
- // Third notification should be ERROR level
- assertThat(notificationMap.get("Another error message").level())
- .isEqualTo(McpSchema.LoggingLevel.ERROR);
- assertThat(notificationMap.get("Another error message").logger()).isEqualTo("test-logger");
- assertThat(notificationMap.get("Another error message").data()).isEqualTo("Another error message");
- });
+ // Third notification should be ERROR level
+ assertThat(notificationMap.get("Another error message").level()).isEqualTo(McpSchema.LoggingLevel.ERROR);
+ assertThat(notificationMap.get("Another error message").logger()).isEqualTo("test-logger");
+ assertThat(notificationMap.get("Another error message").data()).isEqualTo("Another error message");
}
mcpServer.close();
}
@@ -776,7 +778,8 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.capabilities(ServerCapabilities.builder().completions().build())
.prompts(new McpServerFeatures.SyncPromptSpecification(
- new Prompt("code_review", "this is code review prompt", List.of()),
+ new Prompt("code_review", "this is code review prompt",
+ List.of(new PromptArgument("language", "string", false))),
(mcpSyncServerExchange, getPromptRequest) -> null))
.completions(new McpServerFeatures.SyncCompletionSpecification(
new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler))
diff --git a/mcp-spring/mcp-spring-webmvc/pom.xml b/mcp-spring/mcp-spring-webmvc/pom.xml
index b59be6a03..48d1c3465 100644
--- a/mcp-spring/mcp-spring-webmvc/pom.xml
+++ b/mcp-spring/mcp-spring-webmvc/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT../../pom.xmlmcp-spring-webmvc
@@ -25,13 +25,13 @@
io.modelcontextprotocol.sdkmcp
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOTio.modelcontextprotocol.sdkmcp-test
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOTtest
@@ -77,6 +77,12 @@
${mockito.version}test
+
+ net.bytebuddy
+ byte-buddy
+ ${byte-buddy.version}
+ test
+ org.testcontainersjunit-jupiter
diff --git a/mcp-test/pom.xml b/mcp-test/pom.xml
index f1484ae77..a6e5bdb08 100644
--- a/mcp-test/pom.xml
+++ b/mcp-test/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOTmcp-testjar
@@ -24,7 +24,7 @@
io.modelcontextprotocol.sdkmcp
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOT
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
index cdd43e7ef..025cfeacf 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
@@ -30,7 +30,7 @@
/**
* Test suite for the {@link McpAsyncServer} that can be used with different
- * {@link McpTransportProvider} implementations.
+ * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations.
*
* @author Christian Tzolov
*/
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
index c81e638c1..e313454bd 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
@@ -27,7 +27,7 @@
/**
* Test suite for the {@link McpSyncServer} that can be used with different
- * {@link McpTransportProvider} implementations.
+ * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations.
*
* @author Christian Tzolov
*/
diff --git a/mcp/pom.xml b/mcp/pom.xml
index 17693ab32..773432827 100644
--- a/mcp/pom.xml
+++ b/mcp/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.10.0-SNAPSHOT
+ 0.11.0-SNAPSHOTmcpjar
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index 2bc74f258..e3a997ba3 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -317,9 +317,9 @@ public Mono closeGracefully() {
* The client MUST initiate this phase by sending an initialize request containing:
* The protocol version the client supports, client's capabilities and clients
* implementation information.
- *
+ *
* The server MUST respond with its own capabilities and information.
- *
+ *
* After successful initialization, the client MUST send an initialized notification
* to indicate it is ready to begin normal operations.
* @return the initialize result.
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
index c91638a7e..a8fb979e1 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
@@ -97,6 +97,14 @@ public McpSchema.Implementation getServerInfo() {
return this.delegate.getServerInfo();
}
+ /**
+ * Check if the client-server connection is initialized.
+ * @return true if the client-server connection is initialized
+ */
+ public boolean isInitialized() {
+ return this.delegate.isInitialized();
+ }
+
/**
* Get the client capabilities that define the supported features and functionality.
* @return The client capabilities
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index 906cb9a08..1efa13de3 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -5,6 +5,7 @@
package io.modelcontextprotocol.server;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -22,10 +23,13 @@
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
+import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
+import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
+import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -78,11 +82,33 @@ public class McpAsyncServer {
private static final Logger logger = LoggerFactory.getLogger(McpAsyncServer.class);
- private final McpAsyncServer delegate;
+ private final McpServerTransportProvider mcpTransportProvider;
- McpAsyncServer() {
- this.delegate = null;
- }
+ private final ObjectMapper objectMapper;
+
+ private final McpSchema.ServerCapabilities serverCapabilities;
+
+ private final McpSchema.Implementation serverInfo;
+
+ private final String instructions;
+
+ private final CopyOnWriteArrayList tools = new CopyOnWriteArrayList<>();
+
+ private final CopyOnWriteArrayList resourceTemplates = new CopyOnWriteArrayList<>();
+
+ private final ConcurrentHashMap resources = new ConcurrentHashMap<>();
+
+ private final ConcurrentHashMap prompts = new ConcurrentHashMap<>();
+
+ // FIXME: this field is deprecated and should be remvoed together with the
+ // broadcasting loggingNotification.
+ private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
+
+ private final ConcurrentHashMap completions = new ConcurrentHashMap<>();
+
+ private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
+
+ private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
/**
* Create a new McpAsyncServer with the given transport provider and capabilities.
@@ -92,8 +118,106 @@ public class McpAsyncServer {
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
*/
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
- McpServerFeatures.Async features, Duration requestTimeout) {
- this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features);
+ McpServerFeatures.Async features, Duration requestTimeout,
+ McpUriTemplateManagerFactory uriTemplateManagerFactory) {
+ this.mcpTransportProvider = mcpTransportProvider;
+ this.objectMapper = objectMapper;
+ this.serverInfo = features.serverInfo();
+ this.serverCapabilities = features.serverCapabilities();
+ this.instructions = features.instructions();
+ this.tools.addAll(features.tools());
+ this.resources.putAll(features.resources());
+ this.resourceTemplates.addAll(features.resourceTemplates());
+ this.prompts.putAll(features.prompts());
+ this.completions.putAll(features.completions());
+ this.uriTemplateManagerFactory = uriTemplateManagerFactory;
+
+ Map> requestHandlers = new HashMap<>();
+
+ // Initialize request handlers for standard MCP methods
+
+ // Ping MUST respond with an empty data, but not NULL response.
+ requestHandlers.put(McpSchema.METHOD_PING, (exchange, params) -> Mono.just(Map.of()));
+
+ // Add tools API handlers if the tool capability is enabled
+ if (this.serverCapabilities.tools() != null) {
+ requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());
+ requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());
+ }
+
+ // Add resources API handlers if provided
+ if (this.serverCapabilities.resources() != null) {
+ requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());
+ requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());
+ requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
+ }
+
+ // Add prompts API handlers if provider exists
+ if (this.serverCapabilities.prompts() != null) {
+ requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());
+ requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());
+ }
+
+ // Add logging API handlers if the logging capability is enabled
+ if (this.serverCapabilities.logging() != null) {
+ requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
+ }
+
+ // Add completion API handlers if the completion capability is enabled
+ if (this.serverCapabilities.completions() != null) {
+ requestHandlers.put(McpSchema.METHOD_COMPLETION_COMPLETE, completionCompleteRequestHandler());
+ }
+
+ Map notificationHandlers = new HashMap<>();
+
+ notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty());
+
+ List, Mono>> rootsChangeConsumers = features
+ .rootsChangeConsumers();
+
+ if (Utils.isEmpty(rootsChangeConsumers)) {
+ rootsChangeConsumers = List.of((exchange, roots) -> Mono.fromRunnable(() -> logger
+ .warn("Roots list changed notification, but no consumers provided. Roots list changed: {}", roots)));
+ }
+
+ notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
+ asyncRootsListChangedNotificationHandler(rootsChangeConsumers));
+
+ mcpTransportProvider.setSessionFactory(
+ transport -> new McpServerSession(UUID.randomUUID().toString(), requestTimeout, transport,
+ this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers));
+ }
+
+ // ---------------------------------------
+ // Lifecycle Management
+ // ---------------------------------------
+ private Mono asyncInitializeRequestHandler(
+ McpSchema.InitializeRequest initializeRequest) {
+ return Mono.defer(() -> {
+ logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}",
+ initializeRequest.protocolVersion(), initializeRequest.capabilities(),
+ initializeRequest.clientInfo());
+
+ // The server MUST respond with the highest protocol version it supports
+ // if
+ // it does not support the requested (e.g. Client) version.
+ String serverProtocolVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
+
+ if (this.protocolVersions.contains(initializeRequest.protocolVersion())) {
+ // If the server supports the requested protocol version, it MUST
+ // respond
+ // with the same version.
+ serverProtocolVersion = initializeRequest.protocolVersion();
+ }
+ else {
+ logger.warn(
+ "Client requested unsupported protocol version: {}, so the server will suggest the {} version instead",
+ initializeRequest.protocolVersion(), serverProtocolVersion);
+ }
+
+ return Mono.just(new McpSchema.InitializeResult(serverProtocolVersion, this.serverCapabilities,
+ this.serverInfo, this.instructions));
+ });
}
/**
@@ -101,7 +225,7 @@ public class McpAsyncServer {
* @return The server capabilities
*/
public McpSchema.ServerCapabilities getServerCapabilities() {
- return this.delegate.getServerCapabilities();
+ return this.serverCapabilities;
}
/**
@@ -109,7 +233,7 @@ public McpSchema.ServerCapabilities getServerCapabilities() {
* @return The server implementation details
*/
public McpSchema.Implementation getServerInfo() {
- return this.delegate.getServerInfo();
+ return this.serverInfo;
}
/**
@@ -117,26 +241,66 @@ public McpSchema.Implementation getServerInfo() {
* @return A Mono that completes when the server has been closed
*/
public Mono closeGracefully() {
- return this.delegate.closeGracefully();
+ return this.mcpTransportProvider.closeGracefully();
}
/**
* Close the server immediately.
*/
public void close() {
- this.delegate.close();
+ this.mcpTransportProvider.close();
+ }
+
+ private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHandler(
+ List, Mono>> rootsChangeConsumers) {
+ return (exchange, params) -> exchange.listRoots()
+ .flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
+ .flatMap(consumer -> consumer.apply(exchange, listRootsResult.roots()))
+ .onErrorResume(error -> {
+ logger.error("Error handling roots list change notification", error);
+ return Mono.empty();
+ })
+ .then());
}
// ---------------------------------------
// Tool Management
// ---------------------------------------
+
/**
* Add a new tool specification at runtime.
* @param toolSpecification The tool specification to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
- return this.delegate.addTool(toolSpecification);
+ if (toolSpecification == null) {
+ return Mono.error(new McpError("Tool specification must not be null"));
+ }
+ if (toolSpecification.tool() == null) {
+ return Mono.error(new McpError("Tool must not be null"));
+ }
+ if (toolSpecification.call() == null) {
+ return Mono.error(new McpError("Tool call handler must not be null"));
+ }
+ if (this.serverCapabilities.tools() == null) {
+ return Mono.error(new McpError("Server must be configured with tool capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ // Check for duplicate tool names
+ if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolSpecification.tool().name()))) {
+ return Mono
+ .error(new McpError("Tool with name '" + toolSpecification.tool().name() + "' already exists"));
+ }
+
+ this.tools.add(toolSpecification);
+ logger.debug("Added tool handler: {}", toolSpecification.tool().name());
+
+ if (this.serverCapabilities.tools().listChanged()) {
+ return notifyToolsListChanged();
+ }
+ return Mono.empty();
+ });
}
/**
@@ -145,7 +309,25 @@ public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica
* @return Mono that completes when clients have been notified of the change
*/
public Mono removeTool(String toolName) {
- return this.delegate.removeTool(toolName);
+ if (toolName == null) {
+ return Mono.error(new McpError("Tool name must not be null"));
+ }
+ if (this.serverCapabilities.tools() == null) {
+ return Mono.error(new McpError("Server must be configured with tool capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ boolean removed = this.tools
+ .removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName));
+ if (removed) {
+ logger.debug("Removed tool handler: {}", toolName);
+ if (this.serverCapabilities.tools().listChanged()) {
+ return notifyToolsListChanged();
+ }
+ return Mono.empty();
+ }
+ return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
+ });
}
/**
@@ -153,19 +335,65 @@ public Mono removeTool(String toolName) {
* @return A Mono that completes when all clients have been notified
*/
public Mono notifyToolsListChanged() {
- return this.delegate.notifyToolsListChanged();
+ return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
+ }
+
+ private McpServerSession.RequestHandler toolsListRequestHandler() {
+ return (exchange, params) -> {
+ List tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
+
+ return Mono.just(new McpSchema.ListToolsResult(tools, null));
+ };
+ }
+
+ private McpServerSession.RequestHandler toolsCallRequestHandler() {
+ return (exchange, params) -> {
+ McpSchema.CallToolRequest callToolRequest = objectMapper.convertValue(params,
+ new TypeReference() {
+ });
+
+ Optional toolSpecification = this.tools.stream()
+ .filter(tr -> callToolRequest.name().equals(tr.tool().name()))
+ .findAny();
+
+ if (toolSpecification.isEmpty()) {
+ return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
+ }
+
+ return toolSpecification.map(tool -> tool.call().apply(exchange, callToolRequest.arguments()))
+ .orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
+ };
}
// ---------------------------------------
// Resource Management
// ---------------------------------------
+
/**
* Add a new resource handler at runtime.
- * @param resourceHandler The resource handler to add
+ * @param resourceSpecification The resource handler to add
* @return Mono that completes when clients have been notified of the change
*/
- public Mono addResource(McpServerFeatures.AsyncResourceSpecification resourceHandler) {
- return this.delegate.addResource(resourceHandler);
+ public Mono addResource(McpServerFeatures.AsyncResourceSpecification resourceSpecification) {
+ if (resourceSpecification == null || resourceSpecification.resource() == null) {
+ return Mono.error(new McpError("Resource must not be null"));
+ }
+
+ if (this.serverCapabilities.resources() == null) {
+ return Mono.error(new McpError("Server must be configured with resource capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ if (this.resources.putIfAbsent(resourceSpecification.resource().uri(), resourceSpecification) != null) {
+ return Mono.error(new McpError(
+ "Resource with URI '" + resourceSpecification.resource().uri() + "' already exists"));
+ }
+ logger.debug("Added resource handler: {}", resourceSpecification.resource().uri());
+ if (this.serverCapabilities.resources().listChanged()) {
+ return notifyResourcesListChanged();
+ }
+ return Mono.empty();
+ });
}
/**
@@ -174,7 +402,24 @@ public Mono addResource(McpServerFeatures.AsyncResourceSpecification resou
* @return Mono that completes when clients have been notified of the change
*/
public Mono removeResource(String resourceUri) {
- return this.delegate.removeResource(resourceUri);
+ if (resourceUri == null) {
+ return Mono.error(new McpError("Resource URI must not be null"));
+ }
+ if (this.serverCapabilities.resources() == null) {
+ return Mono.error(new McpError("Server must be configured with resource capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ McpServerFeatures.AsyncResourceSpecification removed = this.resources.remove(resourceUri);
+ if (removed != null) {
+ logger.debug("Removed resource handler: {}", resourceUri);
+ if (this.serverCapabilities.resources().listChanged()) {
+ return notifyResourcesListChanged();
+ }
+ return Mono.empty();
+ }
+ return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
+ });
}
/**
@@ -182,19 +427,97 @@ public Mono removeResource(String resourceUri) {
* @return A Mono that completes when all clients have been notified
*/
public Mono notifyResourcesListChanged() {
- return this.delegate.notifyResourcesListChanged();
+ return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
+ }
+
+ private McpServerSession.RequestHandler resourcesListRequestHandler() {
+ return (exchange, params) -> {
+ var resourceList = this.resources.values()
+ .stream()
+ .map(McpServerFeatures.AsyncResourceSpecification::resource)
+ .toList();
+ return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
+ };
+ }
+
+ private McpServerSession.RequestHandler resourceTemplateListRequestHandler() {
+ return (exchange, params) -> Mono
+ .just(new McpSchema.ListResourceTemplatesResult(this.getResourceTemplates(), null));
+
+ }
+
+ private List getResourceTemplates() {
+ var list = new ArrayList<>(this.resourceTemplates);
+ List resourceTemplates = this.resources.keySet()
+ .stream()
+ .filter(uri -> uri.contains("{"))
+ .map(uri -> {
+ var resource = this.resources.get(uri).resource();
+ var template = new McpSchema.ResourceTemplate(resource.uri(), resource.name(), resource.description(),
+ resource.mimeType(), resource.annotations());
+ return template;
+ })
+ .toList();
+
+ list.addAll(resourceTemplates);
+
+ return list;
+ }
+
+ private McpServerSession.RequestHandler resourcesReadRequestHandler() {
+ return (exchange, params) -> {
+ McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params,
+ new TypeReference() {
+ });
+ var resourceUri = resourceRequest.uri();
+
+ McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
+ .stream()
+ .filter(resourceSpecification -> this.uriTemplateManagerFactory
+ .create(resourceSpecification.resource().uri())
+ .matches(resourceUri))
+ .findFirst()
+ .orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
+
+ return specification.readHandler().apply(exchange, resourceRequest);
+ };
}
// ---------------------------------------
// Prompt Management
// ---------------------------------------
+
/**
* Add a new prompt handler at runtime.
* @param promptSpecification The prompt handler to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) {
- return this.delegate.addPrompt(promptSpecification);
+ if (promptSpecification == null) {
+ return Mono.error(new McpError("Prompt specification must not be null"));
+ }
+ if (this.serverCapabilities.prompts() == null) {
+ return Mono.error(new McpError("Server must be configured with prompt capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ McpServerFeatures.AsyncPromptSpecification specification = this.prompts
+ .putIfAbsent(promptSpecification.prompt().name(), promptSpecification);
+ if (specification != null) {
+ return Mono.error(
+ new McpError("Prompt with name '" + promptSpecification.prompt().name() + "' already exists"));
+ }
+
+ logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());
+
+ // Servers that declared the listChanged capability SHOULD send a
+ // notification,
+ // when the list of available prompts changes
+ if (this.serverCapabilities.prompts().listChanged()) {
+ return notifyPromptsListChanged();
+ }
+ return Mono.empty();
+ });
}
/**
@@ -203,7 +526,27 @@ public Mono addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpe
* @return Mono that completes when clients have been notified of the change
*/
public Mono removePrompt(String promptName) {
- return this.delegate.removePrompt(promptName);
+ if (promptName == null) {
+ return Mono.error(new McpError("Prompt name must not be null"));
+ }
+ if (this.serverCapabilities.prompts() == null) {
+ return Mono.error(new McpError("Server must be configured with prompt capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ McpServerFeatures.AsyncPromptSpecification removed = this.prompts.remove(promptName);
+
+ if (removed != null) {
+ logger.debug("Removed prompt handler: {}", promptName);
+ // Servers that declared the listChanged capability SHOULD send a
+ // notification, when the list of available prompts changes
+ if (this.serverCapabilities.prompts().listChanged()) {
+ return this.notifyPromptsListChanged();
+ }
+ return Mono.empty();
+ }
+ return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
+ });
}
/**
@@ -211,7 +554,39 @@ public Mono removePrompt(String promptName) {
* @return A Mono that completes when all clients have been notified
*/
public Mono notifyPromptsListChanged() {
- return this.delegate.notifyPromptsListChanged();
+ return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
+ }
+
+ private McpServerSession.RequestHandler promptsListRequestHandler() {
+ return (exchange, params) -> {
+ // TODO: Implement pagination
+ // McpSchema.PaginatedRequest request = objectMapper.convertValue(params,
+ // new TypeReference() {
+ // });
+
+ var promptList = this.prompts.values()
+ .stream()
+ .map(McpServerFeatures.AsyncPromptSpecification::prompt)
+ .toList();
+
+ return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
+ };
+ }
+
+ private McpServerSession.RequestHandler promptsGetRequestHandler() {
+ return (exchange, params) -> {
+ McpSchema.GetPromptRequest promptRequest = objectMapper.convertValue(params,
+ new TypeReference() {
+ });
+
+ // Implement prompt retrieval logic here
+ McpServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name());
+ if (specification == null) {
+ return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
+ }
+
+ return specification.promptHandler().apply(exchange, promptRequest);
+ };
}
// ---------------------------------------
@@ -231,574 +606,136 @@ public Mono notifyPromptsListChanged() {
*/
@Deprecated
public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) {
- return this.delegate.loggingNotification(loggingMessageNotification);
- }
-
- // ---------------------------------------
- // Sampling
- // ---------------------------------------
- /**
- * This method is package-private and used for test only. Should not be called by user
- * code.
- * @param protocolVersions the Client supported protocol versions.
- */
- void setProtocolVersions(List protocolVersions) {
- this.delegate.setProtocolVersions(protocolVersions);
- }
-
- private static class AsyncServerImpl extends McpAsyncServer {
-
- private final McpServerTransportProvider mcpTransportProvider;
-
- private final ObjectMapper objectMapper;
-
- private final McpSchema.ServerCapabilities serverCapabilities;
-
- private final McpSchema.Implementation serverInfo;
-
- private final String instructions;
-
- private final CopyOnWriteArrayList tools = new CopyOnWriteArrayList<>();
-
- private final CopyOnWriteArrayList resourceTemplates = new CopyOnWriteArrayList<>();
-
- private final ConcurrentHashMap resources = new ConcurrentHashMap<>();
-
- private final ConcurrentHashMap prompts = new ConcurrentHashMap<>();
-
- // FIXME: this field is deprecated and should be remvoed together with the
- // broadcasting loggingNotification.
- private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
-
- private final ConcurrentHashMap completions = new ConcurrentHashMap<>();
-
- private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
-
- AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
- Duration requestTimeout, McpServerFeatures.Async features) {
- this.mcpTransportProvider = mcpTransportProvider;
- this.objectMapper = objectMapper;
- this.serverInfo = features.serverInfo();
- this.serverCapabilities = features.serverCapabilities();
- this.instructions = features.instructions();
- this.tools.addAll(features.tools());
- this.resources.putAll(features.resources());
- this.resourceTemplates.addAll(features.resourceTemplates());
- this.prompts.putAll(features.prompts());
- this.completions.putAll(features.completions());
-
- Map> requestHandlers = new HashMap<>();
-
- // Initialize request handlers for standard MCP methods
-
- // Ping MUST respond with an empty data, but not NULL response.
- requestHandlers.put(McpSchema.METHOD_PING, (exchange, params) -> Mono.just(Map.of()));
-
- // Add tools API handlers if the tool capability is enabled
- if (this.serverCapabilities.tools() != null) {
- requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());
- requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());
- }
-
- // Add resources API handlers if provided
- if (this.serverCapabilities.resources() != null) {
- requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());
- requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());
- requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
- }
-
- // Add prompts API handlers if provider exists
- if (this.serverCapabilities.prompts() != null) {
- requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());
- requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());
- }
-
- // Add logging API handlers if the logging capability is enabled
- if (this.serverCapabilities.logging() != null) {
- requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
- }
- // Add completion API handlers if the completion capability is enabled
- if (this.serverCapabilities.completions() != null) {
- requestHandlers.put(McpSchema.METHOD_COMPLETION_COMPLETE, completionCompleteRequestHandler());
- }
-
- Map notificationHandlers = new HashMap<>();
-
- notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty());
-
- List, Mono>> rootsChangeConsumers = features
- .rootsChangeConsumers();
-
- if (Utils.isEmpty(rootsChangeConsumers)) {
- rootsChangeConsumers = List.of((exchange,
- roots) -> Mono.fromRunnable(() -> logger.warn(
- "Roots list changed notification, but no consumers provided. Roots list changed: {}",
- roots)));
- }
-
- notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
- asyncRootsListChangedNotificationHandler(rootsChangeConsumers));
-
- mcpTransportProvider.setSessionFactory(
- transport -> new McpServerSession(UUID.randomUUID().toString(), requestTimeout, transport,
- this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers));
- }
-
- // ---------------------------------------
- // Lifecycle Management
- // ---------------------------------------
- private Mono asyncInitializeRequestHandler(
- McpSchema.InitializeRequest initializeRequest) {
- return Mono.defer(() -> {
- logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}",
- initializeRequest.protocolVersion(), initializeRequest.capabilities(),
- initializeRequest.clientInfo());
-
- // The server MUST respond with the highest protocol version it supports
- // if
- // it does not support the requested (e.g. Client) version.
- String serverProtocolVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
-
- if (this.protocolVersions.contains(initializeRequest.protocolVersion())) {
- // If the server supports the requested protocol version, it MUST
- // respond
- // with the same version.
- serverProtocolVersion = initializeRequest.protocolVersion();
- }
- else {
- logger.warn(
- "Client requested unsupported protocol version: {}, so the server will suggest the {} version instead",
- initializeRequest.protocolVersion(), serverProtocolVersion);
- }
-
- return Mono.just(new McpSchema.InitializeResult(serverProtocolVersion, this.serverCapabilities,
- this.serverInfo, this.instructions));
- });
- }
-
- public McpSchema.ServerCapabilities getServerCapabilities() {
- return this.serverCapabilities;
- }
-
- public McpSchema.Implementation getServerInfo() {
- return this.serverInfo;
+ if (loggingMessageNotification == null) {
+ return Mono.error(new McpError("Logging message must not be null"));
}
- @Override
- public Mono closeGracefully() {
- return this.mcpTransportProvider.closeGracefully();
+ if (loggingMessageNotification.level().level() < minLoggingLevel.level()) {
+ return Mono.empty();
}
- @Override
- public void close() {
- this.mcpTransportProvider.close();
- }
-
- private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHandler(
- List, Mono>> rootsChangeConsumers) {
- return (exchange, params) -> exchange.listRoots()
- .flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
- .flatMap(consumer -> consumer.apply(exchange, listRootsResult.roots()))
- .onErrorResume(error -> {
- logger.error("Error handling roots list change notification", error);
- return Mono.empty();
- })
- .then());
- }
-
- // ---------------------------------------
- // Tool Management
- // ---------------------------------------
-
- @Override
- public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
- if (toolSpecification == null) {
- return Mono.error(new McpError("Tool specification must not be null"));
- }
- if (toolSpecification.tool() == null) {
- return Mono.error(new McpError("Tool must not be null"));
- }
- if (toolSpecification.call() == null) {
- return Mono.error(new McpError("Tool call handler must not be null"));
- }
- if (this.serverCapabilities.tools() == null) {
- return Mono.error(new McpError("Server must be configured with tool capabilities"));
- }
-
- return Mono.defer(() -> {
- // Check for duplicate tool names
- if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolSpecification.tool().name()))) {
- return Mono
- .error(new McpError("Tool with name '" + toolSpecification.tool().name() + "' already exists"));
- }
-
- this.tools.add(toolSpecification);
- logger.debug("Added tool handler: {}", toolSpecification.tool().name());
-
- if (this.serverCapabilities.tools().listChanged()) {
- return notifyToolsListChanged();
- }
- return Mono.empty();
- });
- }
-
- @Override
- public Mono removeTool(String toolName) {
- if (toolName == null) {
- return Mono.error(new McpError("Tool name must not be null"));
- }
- if (this.serverCapabilities.tools() == null) {
- return Mono.error(new McpError("Server must be configured with tool capabilities"));
- }
+ return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_MESSAGE,
+ loggingMessageNotification);
+ }
+ private McpServerSession.RequestHandler