diff --git a/README.md b/README.md index aa69e8b..9c465bf 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ import io.circe.Json val client = McpClient(transport, Implementation("my-client", "0.1.0")) val result = client.callTool("adder", Json.obj("a" -> Json.fromInt(2), "b" -> Json.fromInt(3))) - val _ = result.content.collect { case ToolContent.Text(_, text) => println(text) } + val _ = result.content.collect { case t: ToolContent.Text => println(t.text) } client.close() backend.close() @@ -96,7 +96,7 @@ import io.circe.Json val client = McpClient(transport, Implementation("my-client", "0.1.0")) val result = client.callTool("echo", Json.obj("message" -> Json.fromString("hello"))) - val _ = result.content.collect { case ToolContent.Text(_, text) => println(text) } + val _ = result.content.collect { case t: ToolContent.Text => println(t.text) } client.close() ``` @@ -160,7 +160,7 @@ import sttp.shared.Identity case _ => () val result = client.callTool("work", Json.obj("steps" -> Json.fromInt(3))) - val _ = result.content.collect { case ToolContent.Text(_, text) => println(text) } + val _ = result.content.collect { case t: ToolContent.Text => println(t.text) } client.close() backend.close() diff --git a/core/src/main/scala/chimp/protocol/Lifecycle.scala b/core/src/main/scala/chimp/protocol/Lifecycle.scala index ed859a1..31bd5d5 100644 --- a/core/src/main/scala/chimp/protocol/Lifecycle.scala +++ b/core/src/main/scala/chimp/protocol/Lifecycle.scala @@ -2,7 +2,14 @@ package chimp.protocol import io.circe.{Codec, Json} -final case class Implementation(name: String, version: String, title: Option[String] = None) derives Codec +final case class Implementation( + name: String, + version: String, + title: Option[String] = None, + icons: Option[List[Icon]] = None, + websiteUrl: Option[String] = None, + description: Option[String] = None +) derives Codec final case class ClientRootsCapability(listChanged: Option[Boolean] = None) derives Codec diff --git a/core/src/main/scala/chimp/protocol/Metadata.scala b/core/src/main/scala/chimp/protocol/Metadata.scala new file mode 100644 index 0000000..a02e28f --- /dev/null +++ b/core/src/main/scala/chimp/protocol/Metadata.scala @@ -0,0 +1,57 @@ +package chimp.protocol + +import io.circe.{Codec, Decoder, Encoder, Json} + +/** The visual theme an [[Icon]] is designed for. Mirrors the closed `["dark", "light"]` string enum declared in both the 2025-11-25 and + * 2026-07-28 MCP schemas, so an invalid value cannot be put on the wire. + */ +enum IconTheme: + case Dark, Light + +object IconTheme: + given Encoder[IconTheme] = Encoder.instance: + case IconTheme.Dark => Json.fromString("dark") + case IconTheme.Light => Json.fromString("light") + given Decoder[IconTheme] = Decoder.decodeString.emap: + case "dark" => Right(IconTheme.Dark) + case "light" => Right(IconTheme.Light) + case other => Left(s"Unknown icon theme: $other") + +/** A relative-importance hint bounded to the schema-mandated 0.0..1.0 range. Constructing or decoding a value outside that range fails, + * which a plain `Double` would silently allow onto the wire. + */ +opaque type Priority = Double +object Priority: + /** Builds a [[Priority]] when `value` is within 0.0..1.0, otherwise returns an error message. */ + def from(value: Double): Either[String, Priority] = + if value >= 0.0 && value <= 1.0 then Right(value) + else Left(s"priority must be within 0.0..1.0, got $value") + + /** Unchecked constructor for in-range literals; throws [[IllegalArgumentException]] for an out-of-range value. */ + def apply(value: Double): Priority = from(value).fold(msg => throw IllegalArgumentException(msg), identity) + + def unapply(priority: Priority): Option[Double] = Some(priority) + + extension (priority: Priority) def value: Double = priority + + given Encoder[Priority] = Encoder.encodeDouble + given Decoder[Priority] = Decoder.decodeDouble.emap(from) + +/** An icon for a resource, tool, prompt, or implementation. `src` is a URI (http(s) or data:); the other fields are optional hints. Present + * on `icons` fields across the shared types. + */ +final case class Icon( + src: String, + mimeType: Option[String] = None, + sizes: Option[List[String]] = None, + theme: Option[IconTheme] = None +) derives Codec + +/** Optional annotations describing how an object or content should be used: its intended `audience`, a `priority` (0.0..1.0) and a + * `lastModified` ISO-8601 timestamp. + */ +final case class Annotations( + audience: Option[List[Role]] = None, + lastModified: Option[String] = None, + priority: Option[Priority] = None +) derives Codec diff --git a/core/src/main/scala/chimp/protocol/Prompts.scala b/core/src/main/scala/chimp/protocol/Prompts.scala index 8db10a3..259821e 100644 --- a/core/src/main/scala/chimp/protocol/Prompts.scala +++ b/core/src/main/scala/chimp/protocol/Prompts.scala @@ -26,6 +26,7 @@ final case class Prompt( title: Option[String] = None, description: Option[String] = None, arguments: Option[List[PromptArgument]] = None, + icons: Option[List[Icon]] = None, _meta: Option[Map[String, Json]] = None ) derives Codec @@ -53,4 +54,4 @@ final case class GetPromptResult( final case class PromptListChangedNotification(method: String = "notifications/prompts/list_changed") derives Codec -final case class PromptReference(`type`: String = "ref/prompt", name: String) derives Codec +final case class PromptReference(`type`: String = "ref/prompt", name: String, title: Option[String] = None) derives Codec diff --git a/core/src/main/scala/chimp/protocol/Resources.scala b/core/src/main/scala/chimp/protocol/Resources.scala index 59901de..807de03 100644 --- a/core/src/main/scala/chimp/protocol/Resources.scala +++ b/core/src/main/scala/chimp/protocol/Resources.scala @@ -10,6 +10,8 @@ final case class Resource( description: Option[String] = None, mimeType: Option[String] = None, size: Option[Long] = None, + icons: Option[List[Icon]] = None, + annotations: Option[Annotations] = None, _meta: Option[Map[String, Json]] = None ) derives Codec @@ -19,6 +21,8 @@ final case class ResourceTemplate( title: Option[String] = None, description: Option[String] = None, mimeType: Option[String] = None, + icons: Option[List[Icon]] = None, + annotations: Option[Annotations] = None, _meta: Option[Map[String, Json]] = None ) derives Codec diff --git a/core/src/main/scala/chimp/protocol/Tools.scala b/core/src/main/scala/chimp/protocol/Tools.scala index 4934942..3873a1c 100644 --- a/core/src/main/scala/chimp/protocol/Tools.scala +++ b/core/src/main/scala/chimp/protocol/Tools.scala @@ -18,6 +18,7 @@ final case class ToolDefinition( outputSchema: Option[Json] = None, title: Option[String] = None, annotations: Option[ToolAnnotations] = None, + icons: Option[List[Icon]] = None, _meta: Option[Map[String, Json]] = None ) derives Codec @@ -32,50 +33,93 @@ final case class ListToolsResponse( ) derives Codec enum ToolContent: - case Text(`type`: String = "text", text: String) - case Image(`type`: String = "image", data: String, mimeType: String) - case Audio(`type`: String = "audio", data: String, mimeType: String) - case ResourceContent(`type`: String = "resource", resource: ResourceContents) + case Text(`type`: String = "text", text: String, annotations: Option[Annotations] = None, _meta: Option[Map[String, Json]] = None) + case Image( + `type`: String = "image", + data: String, + mimeType: String, + annotations: Option[Annotations] = None, + _meta: Option[Map[String, Json]] = None + ) + case Audio( + `type`: String = "audio", + data: String, + mimeType: String, + annotations: Option[Annotations] = None, + _meta: Option[Map[String, Json]] = None + ) + case ResourceContent( + `type`: String = "resource", + resource: ResourceContents, + annotations: Option[Annotations] = None, + _meta: Option[Map[String, Json]] = None + ) case ResourceLink( `type`: String = "resource_link", uri: String, name: Option[String] = None, + title: Option[String] = None, description: Option[String] = None, - mimeType: Option[String] = None + mimeType: Option[String] = None, + size: Option[Long] = None, + icons: Option[List[Icon]] = None, + annotations: Option[Annotations] = None, + _meta: Option[Map[String, Json]] = None ) object ToolContent: given Encoder[ToolContent] = Encoder.instance: - case Text(_, text) => - Json.obj( - "type" -> Json.fromString("text"), - "text" -> Json.fromString(text) - ) - case Image(_, data, mimeType) => - Json.obj( - "type" -> Json.fromString("image"), - "data" -> Json.fromString(data), - "mimeType" -> Json.fromString(mimeType) - ) - case Audio(_, data, mimeType) => - Json.obj( - "type" -> Json.fromString("audio"), - "data" -> Json.fromString(data), - "mimeType" -> Json.fromString(mimeType) - ) - case ResourceContent(_, resource) => - Json.obj( - "type" -> Json.fromString("resource"), - "resource" -> resource.asJson - ) - case ResourceLink(_, uri, name, description, mimeType) => + case Text(_, text, annotations, meta) => + Json + .obj( + "type" -> Json.fromString("text"), + "text" -> Json.fromString(text), + "annotations" -> annotations.asJson, + "_meta" -> meta.asJson + ) + .dropNullValues + case Image(_, data, mimeType, annotations, meta) => + Json + .obj( + "type" -> Json.fromString("image"), + "data" -> Json.fromString(data), + "mimeType" -> Json.fromString(mimeType), + "annotations" -> annotations.asJson, + "_meta" -> meta.asJson + ) + .dropNullValues + case Audio(_, data, mimeType, annotations, meta) => + Json + .obj( + "type" -> Json.fromString("audio"), + "data" -> Json.fromString(data), + "mimeType" -> Json.fromString(mimeType), + "annotations" -> annotations.asJson, + "_meta" -> meta.asJson + ) + .dropNullValues + case ResourceContent(_, resource, annotations, meta) => + Json + .obj( + "type" -> Json.fromString("resource"), + "resource" -> resource.asJson, + "annotations" -> annotations.asJson, + "_meta" -> meta.asJson + ) + .dropNullValues + case ResourceLink(_, uri, name, title, description, mimeType, size, icons, annotations, meta) => Json .obj( "type" -> Json.fromString("resource_link"), "uri" -> Json.fromString(uri), - "name" -> name.map(Json.fromString).getOrElse(Json.Null), - "description" -> description.map(Json.fromString).getOrElse(Json.Null), - "mimeType" -> mimeType.map(Json.fromString).getOrElse(Json.Null) + "name" -> name.asJson, + "title" -> title.asJson, + "description" -> description.asJson, + "mimeType" -> mimeType.asJson, + "size" -> size.asJson, + "icons" -> icons.asJson, + "annotations" -> annotations.asJson, + "_meta" -> meta.asJson ) .dropNullValues @@ -84,26 +128,43 @@ object ToolContent: .as[String] .flatMap: case "text" => - c.downField("text").as[String].map(Text("text", _)) + for + text <- c.downField("text").as[String] + annotations <- c.downField("annotations").as[Option[Annotations]] + meta <- c.downField("_meta").as[Option[Map[String, Json]]] + yield Text("text", text, annotations, meta) case "image" => for data <- c.downField("data").as[String] mimeType <- c.downField("mimeType").as[String] - yield Image("image", data, mimeType) + annotations <- c.downField("annotations").as[Option[Annotations]] + meta <- c.downField("_meta").as[Option[Map[String, Json]]] + yield Image("image", data, mimeType, annotations, meta) case "audio" => for data <- c.downField("data").as[String] mimeType <- c.downField("mimeType").as[String] - yield Audio("audio", data, mimeType) + annotations <- c.downField("annotations").as[Option[Annotations]] + meta <- c.downField("_meta").as[Option[Map[String, Json]]] + yield Audio("audio", data, mimeType, annotations, meta) case "resource" => - c.downField("resource").as[ResourceContents].map(ResourceContent("resource", _)) + for + resource <- c.downField("resource").as[ResourceContents] + annotations <- c.downField("annotations").as[Option[Annotations]] + meta <- c.downField("_meta").as[Option[Map[String, Json]]] + yield ResourceContent("resource", resource, annotations, meta) case "resource_link" => for uri <- c.downField("uri").as[String] name <- c.downField("name").as[Option[String]] + title <- c.downField("title").as[Option[String]] description <- c.downField("description").as[Option[String]] mimeType <- c.downField("mimeType").as[Option[String]] - yield ResourceLink("resource_link", uri, name, description, mimeType) + size <- c.downField("size").as[Option[Long]] + icons <- c.downField("icons").as[Option[List[Icon]]] + annotations <- c.downField("annotations").as[Option[Annotations]] + meta <- c.downField("_meta").as[Option[Map[String, Json]]] + yield ResourceLink("resource_link", uri, name, title, description, mimeType, size, icons, annotations, meta) case other => Left(DecodingFailure(s"Unknown ToolContent type: $other", c.history)) diff --git a/core/src/test/scala/chimp/protocol/SchemaConformance.scala b/core/src/test/scala/chimp/protocol/SchemaConformance.scala index ed3810e..59c6a7c 100644 --- a/core/src/test/scala/chimp/protocol/SchemaConformance.scala +++ b/core/src/test/scala/chimp/protocol/SchemaConformance.scala @@ -47,3 +47,74 @@ trait SchemaConformance extends AnyFlatSpec with Matchers: decoded shouldBe value case Left(err) => fail(s"Decode round-trip failed for $defName:\nencoded: $encodedStr\nerror: ${err.getMessage}") + + // Shared metadata types (icons, annotations, _meta, title) are byte-identical across the 2025-11-25 and 2026-07-28 schemas, so both + // concrete specs validate them. A field regression that breaks the older revision must not be invisible (AGENTS.md rule 2). + private val sampleIcon = + Icon(src = "https://example.com/icon.png", mimeType = Some("image/png"), sizes = Some(List("48x48")), theme = Some(IconTheme.Dark)) + + it should "produce an Icon that matches the spec schema" in: + validate("Icon", sampleIcon) + + it should "produce Annotations that match the spec schema" in: + validate( + "Annotations", + Annotations( + audience = Some(List(Role.User, Role.Assistant)), + lastModified = Some("2026-01-12T15:00:58Z"), + priority = Some(Priority(0.5)) + ) + ) + + it should "produce an Implementation with icons, websiteUrl and description that matches the spec schema" in: + validate( + "Implementation", + Implementation( + "chimp", + "1.0", + title = Some("Chimp"), + icons = Some(List(sampleIcon)), + websiteUrl = Some("https://example.com"), + description = Some("test") + ) + ) + + it should "produce a Tool with icons that matches the spec schema" in: + validate( + "Tool", + ToolDefinition(name = "add", inputSchema = io.circe.Json.obj("type" -> "object".asJson), icons = Some(List(sampleIcon))) + ) + + it should "produce a Resource with icons and annotations that matches the spec schema" in: + validate( + "Resource", + Resource( + uri = "file:///x", + name = "x", + icons = Some(List(sampleIcon)), + annotations = Some(Annotations(priority = Some(Priority(0.3)))) + ) + ) + + it should "produce a Prompt with icons that matches the spec schema" in: + validate("Prompt", Prompt(name = "greet", icons = Some(List(sampleIcon)))) + + it should "produce a PromptReference with title that matches the spec schema" in: + validate("PromptReference", PromptReference(name = "greet", title = Some("Greet"))) + + it should "produce a ResourceLink with title, size, icons and annotations that matches the spec schema" in: + val link: ToolContent = ToolContent.ResourceLink( + uri = "file:///x", + name = Some("x"), + title = Some("X"), + description = Some("a link"), + mimeType = Some("text/plain"), + size = Some(10L), + icons = Some(List(sampleIcon)), + annotations = Some(Annotations(priority = Some(Priority(0.1)))) + ) + validate("ResourceLink", link) + + it should "produce TextContent with annotations that matches the spec schema" in: + val text: ToolContent = ToolContent.Text(text = "hi", annotations = Some(Annotations(audience = Some(List(Role.Assistant))))) + validate("TextContent", text) diff --git a/docs/client/examples.md b/docs/client/examples.md index c253e1a..a89d7b5 100644 --- a/docs/client/examples.md +++ b/docs/client/examples.md @@ -20,7 +20,7 @@ object HttpClient: val client = McpClient[Identity](transport, Implementation("my-client", "0.1.0")) val result = client.callTool("adder", Json.obj("a" -> Json.fromInt(2), "b" -> Json.fromInt(3))) - result.content.collect { case ToolContent.Text(_, text) => text }.foreach(println) + result.content.collect { case t: ToolContent.Text => t.text }.foreach(println) client.close() backend.close() @@ -43,7 +43,7 @@ object StdioClient: val client = McpClient[Identity](transport, Implementation("my-client", "0.1.0")) val result = client.callTool("adder", Json.obj("a" -> Json.fromInt(2), "b" -> Json.fromInt(3))) - result.content.collect { case ToolContent.Text(_, text) => text }.foreach(println) + result.content.collect { case t: ToolContent.Text => t.text }.foreach(println) client.close() ``` diff --git a/docs/client/quickstart.md b/docs/client/quickstart.md index d8026fb..1e65c60 100644 --- a/docs/client/quickstart.md +++ b/docs/client/quickstart.md @@ -28,7 +28,7 @@ object QuickstartClient: val client = McpClient[Identity](transport, Implementation("my-client", "0.1.0")) val result = client.callTool("adder", Json.obj("a" -> Json.fromInt(2), "b" -> Json.fromInt(3))) - result.content.collect { case ToolContent.Text(_, text) => text }.foreach(println) + result.content.collect { case t: ToolContent.Text => t.text }.foreach(println) client.close() backend.close() diff --git a/examples/src/main/scala/examples/both/serverAndClient.scala b/examples/src/main/scala/examples/both/serverAndClient.scala index 6ed3b28..94cdd5a 100644 --- a/examples/src/main/scala/examples/both/serverAndClient.scala +++ b/examples/src/main/scala/examples/both/serverAndClient.scala @@ -33,7 +33,7 @@ case class AddInput(a: Int, b: Int) derives Codec, Schema val tools = client.listTools() println(s"tools: ${tools.tools.map(_.name).mkString(", ")}") val result = client.callTool("adder", Json.obj("a" -> Json.fromInt(2), "b" -> Json.fromInt(3))) - result.content.collect { case ToolContent.Text(_, text) => text }.foreach(text => println(s"2 + 3 = $text")) + result.content.collect { case ToolContent.Text(_, text, _, _) => text }.foreach(text => println(s"2 + 3 = $text")) client.close() finally backend.close() finally binding.stop() diff --git a/examples/src/main/scala/examples/both/streamingServerAndClient.scala b/examples/src/main/scala/examples/both/streamingServerAndClient.scala index 7f76df7..66f247d 100644 --- a/examples/src/main/scala/examples/both/streamingServerAndClient.scala +++ b/examples/src/main/scala/examples/both/streamingServerAndClient.scala @@ -43,7 +43,7 @@ case class NoInput() derives Codec, Schema case ServerNotification.LoggingMessage(params) => println(s"notification: ${params.data}") case _ => () val result = client.callTool("noisy", Json.obj()) - result.content.collect { case ToolContent.Text(_, text) => text }.foreach(text => println(s"result: $text")) + result.content.collect { case ToolContent.Text(_, text, _, _) => text }.foreach(text => println(s"result: $text")) client.close() finally backend.close() finally binding.stop() diff --git a/examples/src/main/scala/examples/client/everythingClient.scala b/examples/src/main/scala/examples/client/everythingClient.scala index 4a6be43..fa857ce 100644 --- a/examples/src/main/scala/examples/client/everythingClient.scala +++ b/examples/src/main/scala/examples/client/everythingClient.scala @@ -34,14 +34,14 @@ import sttp.shared.Identity println("\n--- calling echo ---") val echoResult = client.callTool("echo", Json.obj("message" -> Json.fromString("hello chimp"))) echoResult.content.foreach: - case ToolContent.Text(_, text) => println(text) - case other => println(s"(non-text content: $other)") + case ToolContent.Text(_, text, _, _) => println(text) + case other => println(s"(non-text content: $other)") println("\n--- calling get-sum ---") val sumResult = client.callTool("get-sum", Json.obj("a" -> Json.fromInt(7), "b" -> Json.fromInt(35))) sumResult.content.foreach: - case ToolContent.Text(_, text) => println(text) - case other => println(s"(non-text content: $other)") + case ToolContent.Text(_, text, _, _) => println(text) + case other => println(s"(non-text content: $other)") finally client.close() backend.close() diff --git a/server/src/main/scala/chimp/server/Prompt.scala b/server/src/main/scala/chimp/server/Prompt.scala index 8bbbc8c..fa3264c 100644 --- a/server/src/main/scala/chimp/server/Prompt.scala +++ b/server/src/main/scala/chimp/server/Prompt.scala @@ -1,6 +1,6 @@ package chimp.server -import chimp.protocol.{GetPromptResult, Prompt, PromptArgument} +import chimp.protocol.{GetPromptResult, Icon, Prompt, PromptArgument} import sttp.model.Header import sttp.shared.Identity @@ -12,7 +12,8 @@ case class PartialPrompt( name: String, title: Option[String] = None, description: Option[String] = None, - arguments: List[PromptArgument] = Nil + arguments: List[PromptArgument] = Nil, + icons: Option[List[Icon]] = None ): def title(value: String): PartialPrompt = copy(title = Some(value)) @@ -20,6 +21,9 @@ case class PartialPrompt( def description(value: String): PartialPrompt = copy(description = Some(value)) + def icons(value: List[Icon]): PartialPrompt = + copy(icons = Some(value)) + /** Declares a single argument the prompt accepts. */ def argument(name: String, description: Option[String] = None, required: Boolean = false): PartialPrompt = copy(arguments = arguments :+ PromptArgument(name, description, required = Some(required))) @@ -57,7 +61,7 @@ case class PartialPrompt( securedServerLogic[Identity, P]((args, principal, _) => logic(args, principal)) private def definition: Prompt = - Prompt(name, title, description, Option.when(arguments.nonEmpty)(arguments)) + Prompt(name, title, description, Option.when(arguments.nonEmpty)(arguments), icons = icons) end PartialPrompt diff --git a/server/src/main/scala/chimp/server/Resource.scala b/server/src/main/scala/chimp/server/Resource.scala index ec37ce8..ad645a6 100644 --- a/server/src/main/scala/chimp/server/Resource.scala +++ b/server/src/main/scala/chimp/server/Resource.scala @@ -1,6 +1,6 @@ package chimp.server -import chimp.protocol.{Resource, ResourceContents, ResourceTemplate} +import chimp.protocol.{Annotations, Icon, Resource, ResourceContents, ResourceTemplate} import sttp.model.Header import sttp.shared.Identity @@ -23,7 +23,9 @@ case class PartialResource( title: Option[String] = None, description: Option[String] = None, mimeType: Option[String] = None, - size: Option[Long] = None + size: Option[Long] = None, + icons: Option[List[Icon]] = None, + annotations: Option[Annotations] = None ): def name(value: String): PartialResource = copy(name = Some(value)) @@ -40,6 +42,12 @@ case class PartialResource( def size(value: Long): PartialResource = copy(size = Some(value)) + def icons(value: List[Icon]): PartialResource = + copy(icons = Some(value)) + + def annotations(value: Annotations): PartialResource = + copy(annotations = Some(value)) + /** Attaches effectful logic, with access to the request headers, producing the resource's contents (or an error). */ def serverLogic[F[_]]( logic: Seq[Header] => F[Either[ResourceError, List[ResourceContents]]] @@ -68,7 +76,7 @@ case class PartialResource( ): ServerResource[Identity, SecuredServerContext[Identity, P]] = securedServerLogic[Identity, P]((principal, _) => logic(principal)) - private def definition: Resource = Resource(uri, name.getOrElse(uri), title, description, mimeType, size) + private def definition: Resource = Resource(uri, name.getOrElse(uri), title, description, mimeType, size, icons, annotations) end PartialResource @@ -84,7 +92,9 @@ case class PartialResourceTemplate( name: Option[String] = None, title: Option[String] = None, description: Option[String] = None, - mimeType: Option[String] = None + mimeType: Option[String] = None, + icons: Option[List[Icon]] = None, + annotations: Option[Annotations] = None ): def name(value: String): PartialResourceTemplate = copy(name = Some(value)) @@ -98,6 +108,12 @@ case class PartialResourceTemplate( def mimeType(value: String): PartialResourceTemplate = copy(mimeType = Some(value)) + def icons(value: List[Icon]): PartialResourceTemplate = + copy(icons = Some(value)) + + def annotations(value: Annotations): PartialResourceTemplate = + copy(annotations = Some(value)) + /** Attaches effectful logic reading a matched URI; receives the extracted variables, the full URI, and the request headers. */ def serverLogic[F[_]]( logic: (Map[String, String], String, Seq[Header]) => F[Either[ResourceError, List[ResourceContents]]] @@ -133,7 +149,7 @@ case class PartialResourceTemplate( securedServerLogic[Identity, P]((vars, uri, principal, _) => logic(vars, uri, principal)) private def definition: ResourceTemplate = - ResourceTemplate(uriTemplate, name.getOrElse(uriTemplate), title, description, mimeType) + ResourceTemplate(uriTemplate, name.getOrElse(uriTemplate), title, description, mimeType, icons, annotations) end PartialResourceTemplate diff --git a/server/src/test/scala/chimp/server/McpHandlerSpec.scala b/server/src/test/scala/chimp/server/McpHandlerSpec.scala index 151fa0b..9ae6141 100644 --- a/server/src/test/scala/chimp/server/McpHandlerSpec.scala +++ b/server/src/test/scala/chimp/server/McpHandlerSpec.scala @@ -572,8 +572,8 @@ class McpHandlerSpec extends AnyFlatSpec with Matchers: featureResult("prompts/get", Some(params), "p2") match case Response(_, _, result) => result.as[GetPromptResult].getOrElse(fail("result")).messages.head.content match - case ToolContent.Text(_, text) => text should include("World") - case _ => fail("Expected text content") + case ToolContent.Text(_, text, _, _) => text should include("World") + case _ => fail("Expected text content") case _ => fail("Expected Response") it should "return completion values" in: diff --git a/server/src/test/scala/chimp/server/McpServerTests.scala b/server/src/test/scala/chimp/server/McpServerTests.scala index 58ef073..b275149 100644 --- a/server/src/test/scala/chimp/server/McpServerTests.scala +++ b/server/src/test/scala/chimp/server/McpServerTests.scala @@ -74,8 +74,8 @@ trait McpServerTests[F[_]] extends AsyncFlatSpec with Matchers: .getPrompt("greet", Map("name" -> "World")) .map: result => result.messages.head.content match - case ToolContent.Text(_, text) => text should include("World") - case other => fail(s"expected text content, got $other") + case ToolContent.Text(_, text, _, _) => text should include("World") + case other => fail(s"expected text content, got $other") it should "return completion suggestions" in withServer(sampleServer): client => client