Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,41 @@ libraryDependencies += "com.tjclp" %% "fast-mcp-scala" % "0.1.1"

```scala
//> using scala 3.6.4
//> using dep com.tjclp::fast-mcp-scala:0.1.1
//> using dep com.tjclp::fast-mcp-scala:0.1.2-SNAPSHOT
//> using options "-Xcheck-macros" "-experimental"

import com.tjclp.fastmcp.core.{Tool, ToolParam, Prompt, PromptParam, Resource}
import com.tjclp.fastmcp.core.{Tool, ToolParam, Prompt, PromptParam, Resource, ResourceParam}
import com.tjclp.fastmcp.server.FastMcpServer
import com.tjclp.fastmcp.macros.RegistrationMacro.*
import zio.*

// Define annotated tools, prompts, and resources
object Example:
@Tool(name = Some("add"), description = Some("Add two numbers"))
def add(
@ToolParam("First operand") a: Double,
@ToolParam("Second operand") b: Double
): Double = a + b

@Prompt(name = Some("greet"), description = Some("Generate a greeting message"))
def greet(@PromptParam("Name to greet") name: String): String =
s"Hello, $name!"
@Tool(name = Some("add"), description = Some("Add two numbers"))
def add(
@ToolParam("First operand") a: Double,
@ToolParam("Second operand") b: Double
): Double = a + b

@Prompt(name = Some("greet"), description = Some("Generate a greeting message"))
def greet(@PromptParam("Name to greet") name: String): String =
s"Hello, $name!"

// Note: resource templates (templated URIs) are not yet supported;
// coming soon when the MCP java‑sdk adds template support.
@Resource(uri = "file://test", description = Some("Test resource"))
def test(): String = "This is a test"
@Resource(uri = "file://test", description = Some("Test resource"))
def test(): String = "This is a test"

@Resource(uri = "user://{userId}", description = Some("Test resource"))
def getUser(@ResourceParam("The user id") userId: String): String = s"User ID: $userId"

object ExampleServer extends ZIOAppDefault:
override def run =
for
server <- ZIO.succeed(FastMcpServer("ExampleServer"))
_ <- ZIO.attempt(server.scanAnnotations[Example.type])
_ <- server.runStdio()
yield ()

override def run =
for
server <- ZIO.succeed(FastMcpServer("ExampleServer", "0.1.1"))
_ <- ZIO.attempt(server.scanAnnotations[Example.type])
_ <- server.runStdio()
yield ()
```

### Running Examples
Expand Down
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ lazy val Versions = new {
val jackson = "2.18.3"
val tapir = "1.11.25"
val jsonSchemaCirce = "0.11.9"
val mcpSdk = "0.9.0"
val mcpSdk = "0.11.3"
val scalaTest = "3.2.19"
}

Expand All @@ -33,7 +33,7 @@ lazy val root = (project in file("."))
.settings(
name := "fast-mcp-scala",
// Enable Scala 3 macros with reasonable inline limits for better compilation performance
// resolvers += Resolver.mavenLocal,
resolvers += Resolver.mavenLocal,
scalacOptions ++= Seq("-Xcheck-macros", "-experimental", "-Xmax-inlines:128"),
ThisBuild / scalafmtOnCompile := true,
semanticdbEnabled := true,
Expand Down
10 changes: 6 additions & 4 deletions scripts/quickstart.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//> using scala 3.6.4
//> using dep com.tjclp::fast-mcp-scala:0.1.1
//> using repository m2Local
//> using dep com.tjclp::fast-mcp-scala:0.1.2-SNAPSHOT
//> using options "-Xcheck-macros" "-experimental"

import com.tjclp.fastmcp.core.{Tool, ToolParam, Prompt, PromptParam, Resource}
import com.tjclp.fastmcp.core.{Tool, ToolParam, Prompt, PromptParam, Resource, ResourceParam}
import com.tjclp.fastmcp.server.FastMcpServer
import com.tjclp.fastmcp.macros.RegistrationMacro.*
import zio.*
Expand All @@ -20,11 +21,12 @@ object Example:
def greet(@PromptParam("Name to greet") name: String): String =
s"Hello, $name!"

// Note: resource templates (templated URIs) are not yet supported;
// coming soon when the MCP java‑sdk adds template support.
@Resource(uri = "file://test", description = Some("Test resource"))
def test(): String = "This is a test"

@Resource(uri = "user://{userId}", description = Some("Test resource"))
def getUser(@ResourceParam("The user id") userId: String): String = s"User ID: $userId"

object ExampleServer extends ZIOAppDefault:

override def run =
Expand Down
41 changes: 15 additions & 26 deletions src/main/scala/com/tjclp/fastmcp/core/Types.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.tjclp.fastmcp.core

import io.modelcontextprotocol.spec.McpSchema
import io.modelcontextprotocol.spec.McpSchema.Tool
import zio.json.*

import scala.jdk.CollectionConverters.* // For Java/Scala collection conversions
Expand Down Expand Up @@ -38,27 +39,16 @@ object ToolDefinition:

// Helper to convert to Java SDK Tool
def toJava(td: ToolDefinition): McpSchema.Tool =
val tool = td.inputSchema match {
val baseToolBuilder = Tool.Builder().name(td.name).description(td.description.orNull)
val toolBuilder = td.inputSchema match {
case Left(mcpSchema) =>
// Directly use McpSchema.JsonSchema
new McpSchema.Tool(
td.name,
td.description.orNull,
mcpSchema
)
baseToolBuilder.inputSchema(mcpSchema)
case Right(stringSchema) =>
// Use string schema - MCP SDK will parse it
new McpSchema.Tool(
td.name,
td.description.orNull,
stringSchema
)
baseToolBuilder.inputSchema(stringSchema)
}

// Add any additional properties via setters if needed
// (will depend on future Java SDK enhancements)

tool
toolBuilder.build()

// --- Resource Related Types ---
// REMOVED ResourceDefinition case class and companion object from here.
Expand Down Expand Up @@ -108,11 +98,11 @@ case class TextContent(
) extends Content("text"):

override def toJava: McpSchema.TextContent =
new McpSchema.TextContent(
val ann = new McpSchema.Annotations(
audience.map(roles => roles.map(Role.toJava).asJava).orNull,
priority.map(Double.box).orNull,
text
priority.map(Double.box).orNull
)
new McpSchema.TextContent(ann, text)

object TextContent:
given JsonCodec[TextContent] = DeriveJsonCodec.gen[TextContent]
Expand All @@ -125,12 +115,11 @@ case class ImageContent(
) extends Content("image"):

override def toJava: McpSchema.ImageContent =
new McpSchema.ImageContent(
val ann = new McpSchema.Annotations(
audience.map(roles => roles.map(Role.toJava).asJava).orNull,
priority.map(Double.box).orNull,
data,
mimeType
priority.map(Double.box).orNull
)
new McpSchema.ImageContent(ann, data, mimeType)

object ImageContent:
given JsonCodec[ImageContent] = DeriveJsonCodec.gen[ImageContent]
Expand Down Expand Up @@ -159,11 +148,11 @@ case class EmbeddedResource(
) extends Content("resource"):

override def toJava: McpSchema.EmbeddedResource =
new McpSchema.EmbeddedResource(
val ann = new McpSchema.Annotations(
audience.map(roles => roles.map(Role.toJava).asJava).orNull,
priority.map(Double.box).orNull,
resource.toJava
priority.map(Double.box).orNull
)
new McpSchema.EmbeddedResource(ann, resource.toJava)

object EmbeddedResource:
given JsonCodec[EmbeddedResource] = DeriveJsonCodec.gen[EmbeddedResource]
Expand Down
52 changes: 45 additions & 7 deletions src/main/scala/com/tjclp/fastmcp/examples/AnnotatedServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,24 +104,62 @@ object AnnotatedServer extends ZIOAppDefault:
def welcomeResource(): String =
"Welcome to the FastMCP-Scala Annotated Server!"

/** A template resource that takes a user ID from the URI. Annotated with @Resource. The URI
* pattern {userId} matches the parameter name.
/** A template resource that takes a user ID from the URI. The URI pattern {userId} matches the
* parameter name.
*/
@Resource(
uri = "users://profile",
uri = "users://{userId}/profile",
name = Some("UserProfile"),
description = Some("Dynamically generated user profile."),
description = Some("Dynamically generated user profile based on user ID."),
mimeType = Some("application/json")
)
def userProfileResource(): String =
def userProfileResource(
@ResourceParam("The unique identifier of the user") userId: String
): String =
// In a real app, fetch user data based on userId
val userId = "123"
Map(
"userId" -> userId,
"name" -> s"User $userId",
"email" -> s"[email protected]"
"email" -> s"[email protected]",
"joined" -> "2024-01-15"
).toJsonPretty

/** A template resource demonstrating multiple path parameters.
*/
@Resource(
uri = "repos://{owner}/{repo}/issues/{id}",
name = Some("RepoIssue"),
description = Some("Get a specific issue from a repository."),
mimeType = Some("application/json")
)
def getRepositoryIssue(
@ResourceParam("Repository owner") owner: String,
@ResourceParam("Repository name") repo: String,
@ResourceParam("Issue ID") id: String
): String =
Map(
"owner" -> owner,
"repo" -> repo,
"id" -> id,
"title" -> s"Issue #$id in $owner/$repo",
"status" -> "open",
"created" -> "2024-06-01"
).toJsonPretty

/** A template resource for file access with custom MIME type detection.
*/
@Resource(
uri = "files://{path}",
name = Some("FileContent"),
description = Some("Read file content from a specific path.")
)
def readFileResource(
@ResourceParam("File path relative to the server root") path: String
): String =
// In a real implementation, you would read the actual file
// For demo purposes, we'll return mock content
s"Content of file: $path\n\nThis is a demo file content."

/** A simple prompt with no arguments. Annotated with @Prompt.
*/
@Prompt(
Expand Down
Loading