diff --git a/README.md b/README.md index 359cb80..6cf42e3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ libraryDependencies += "com.tjclp" %% "fast-mcp-scala" % "0.2.0" //> using dep com.tjclp::fast-mcp-scala:0.2.0 //> using options "-Xcheck-macros" "-experimental" -import com.tjclp.fastmcp.core.{Tool, ToolParam, Prompt, PromptParam, Resource, ResourceParam} +import com.tjclp.fastmcp.core.{Tool, Param, Prompt, Resource} import com.tjclp.fastmcp.server.FastMcpServer import com.tjclp.fastmcp.macros.RegistrationMacro.* import zio.* @@ -32,19 +32,19 @@ import zio.* object Example: @Tool(name = Some("add"), description = Some("Add two numbers")) def add( - @ToolParam("First operand") a: Double, - @ToolParam("Second operand") b: Double + @Param("First operand") a: Double, + @Param("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 = + def greet(@Param("Name to greet") name: String): String = s"Hello, $name!" @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" + def getUser(@Param("The user id") userId: String): String = s"User ID: $userId" object ExampleServer extends ZIOAppDefault: override def run = diff --git a/scripts/quickstart.sc b/scripts/quickstart.sc index 9643344..a587c5a 100644 --- a/scripts/quickstart.sc +++ b/scripts/quickstart.sc @@ -1,8 +1,8 @@ //> using scala 3.7.2 -//> using dep com.tjclp::fast-mcp-scala:0.2.0 +//> using dep com.tjclp::fast-mcp-scala:0.2.1-SNAPSHOT //> using options "-Xcheck-macros" "-experimental" -import com.tjclp.fastmcp.core.{Tool, ToolParam, Prompt, PromptParam, Resource, ResourceParam} +import com.tjclp.fastmcp.core.{Tool, Param, Prompt, Resource} import com.tjclp.fastmcp.server.FastMcpServer import com.tjclp.fastmcp.macros.RegistrationMacro.* import zio.* @@ -12,19 +12,19 @@ object Example: @Tool(name = Some("add"), description = Some("Add two numbers")) def add( - @ToolParam("First operand") a: Double, - @ToolParam("Second operand") b: Double + @Param("First operand") a: Double, + @Param("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 = + def greet(@Param("Name to greet") name: String): String = s"Hello, $name!" @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" + def getUser(@Param("The user id") userId: String): String = s"User ID: $userId" object ExampleServer extends ZIOAppDefault: diff --git a/src/main/scala/com/tjclp/fastmcp/core/Annotations.scala b/src/main/scala/com/tjclp/fastmcp/core/Annotations.scala index ac0e512..8b1f49f 100644 --- a/src/main/scala/com/tjclp/fastmcp/core/Annotations.scala +++ b/src/main/scala/com/tjclp/fastmcp/core/Annotations.scala @@ -40,6 +40,28 @@ class Tool( val timeoutMillis: Option[Long] = None ) extends StaticAnnotation +/** Unified annotation for method parameters across Tools, Resources, and Prompts + * + * This annotation can be used with any @Tool, @Resource, or @Prompt annotated method to provide + * descriptions and metadata for parameters. + * + * @param description + * Description of the parameter for documentation + * @param example + * Optional example value for the parameter + * @param required + * Whether the parameter is required (defaults to true) + * @param schema + * Optional JSON schema override for the parameter type + * @since 0.2.1 + */ +class Param( + val description: String, + val example: Option[String] = None, + val required: Boolean = true, + val schema: Option[String] = None +) extends StaticAnnotation + /** Annotation for method parameters in Tool methods * * @param description @@ -50,7 +72,10 @@ class Tool( * Whether the parameter is required (defaults to true) * @param schema * Optional JSON schema override for the parameter type + * @deprecated + * Use [[Param]] instead. Will be removed in 0.3.0. */ +@deprecated("Use @Param instead", "0.2.1") class ToolParam( val description: String, val example: Option[String] = None, @@ -86,7 +111,10 @@ class Resource( * A human-readable description for the parameter * @param required * Whether the parameter is required (defaults to true) + * @deprecated + * Use [[Param]] instead. Will be removed in 0.3.0. */ +@deprecated("Use @Param instead", "0.2.1") class ResourceParam( val description: String, val required: Boolean = true @@ -110,7 +138,10 @@ class Prompt( * Description of the parameter for prompt documentation * @param required * Whether the parameter is required (defaults to true) + * @deprecated + * Use [[Param]] instead. Will be removed in 0.3.0. */ +@deprecated("Use @Param instead", "0.2.1") class PromptParam( val description: String, val required: Boolean = true diff --git a/src/main/scala/com/tjclp/fastmcp/examples/AnnotatedServer.scala b/src/main/scala/com/tjclp/fastmcp/examples/AnnotatedServer.scala index cac37e3..4437dd9 100644 --- a/src/main/scala/com/tjclp/fastmcp/examples/AnnotatedServer.scala +++ b/src/main/scala/com/tjclp/fastmcp/examples/AnnotatedServer.scala @@ -35,7 +35,7 @@ object AnnotatedServer extends ZIOAppDefault: @Tool(name = Some("description")) def generateDescription( - @ToolParam("A description to generate") description: Description + @Param("A description to generate") description: Description ): String = if description.isUpper then description.text.toUpperCase else description.text @@ -48,8 +48,8 @@ object AnnotatedServer extends ZIOAppDefault: // description = Some("Add two numbers together") ) def add( - @ToolParam("First number") a: Int, - @ToolParam("Second number") b: Int + @Param("First number") a: Int, + @Param("Second number") b: Int ): Int = a + b /** More complex calculator tool that handles different operations. @@ -60,9 +60,9 @@ object AnnotatedServer extends ZIOAppDefault: tags = List("math", "calculation") ) def calculate( - @ToolParam("First number") a: Double, - @ToolParam("Second number") b: Double, - @ToolParam( + @Param("First number") a: Double, + @Param("Second number") b: Double, + @Param( "Operation to perform (add, subtract, multiply, divide)", required = false ) operation: String = "add" @@ -114,7 +114,7 @@ object AnnotatedServer extends ZIOAppDefault: mimeType = Some("application/json") ) def userProfileResource( - @ResourceParam("The unique identifier of the user") userId: String + @Param("The unique identifier of the user") userId: String ): String = // In a real app, fetch user data based on userId Map( @@ -133,9 +133,9 @@ object AnnotatedServer extends ZIOAppDefault: mimeType = Some("application/json") ) def getRepositoryIssue( - @ResourceParam("Repository owner") owner: String, - @ResourceParam("Repository name") repo: String, - @ResourceParam("Issue ID") id: String + @Param("Repository owner") owner: String, + @Param("Repository name") repo: String, + @Param("Issue ID") id: String ): String = Map( "owner" -> owner, diff --git a/src/main/scala/com/tjclp/fastmcp/macros/MacroUtils.scala b/src/main/scala/com/tjclp/fastmcp/macros/MacroUtils.scala index 1769809..04a69b6 100644 --- a/src/main/scala/com/tjclp/fastmcp/macros/MacroUtils.scala +++ b/src/main/scala/com/tjclp/fastmcp/macros/MacroUtils.scala @@ -135,6 +135,26 @@ private[macros] object MacroUtils: case None => (None, true) // Defaults if no @PromptParam } + // Generic helper to extract parameter annotations (Param or legacy specific ones) + // First checks for new @Param, then falls back to context-specific annotation + def extractParamAnnotation(using quotes: Quotes)( + sym: quotes.reflect.Symbol, + fallbackAnnotationType: Option[String] = None + ): Option[quotes.reflect.Term] = + import quotes.reflect.* + + // First check for the new unified @Param annotation + val paramAnnot = extractAnnotation[com.tjclp.fastmcp.core.Param](sym) + if (paramAnnot.isDefined) return paramAnnot + + // Fall back to specific annotations based on context + fallbackAnnotationType match { + case Some("Tool") => extractAnnotation[com.tjclp.fastmcp.core.ToolParam](sym) + case Some("Resource") => extractAnnotation[com.tjclp.fastmcp.core.ResourceParam](sym) + case Some("Prompt") => extractAnnotation[com.tjclp.fastmcp.core.PromptParam](sym) + case _ => None + } + // Helper to parse @Param annotation arguments for @Tool methods // Returns: (description: Option[String], example: Option[String], required: Boolean, schema: Option[String]) def parseToolParam(using quotes: Quotes)( diff --git a/src/main/scala/com/tjclp/fastmcp/macros/PromptProcessor.scala b/src/main/scala/com/tjclp/fastmcp/macros/PromptProcessor.scala index c438972..ab1fbf0 100644 --- a/src/main/scala/com/tjclp/fastmcp/macros/PromptProcessor.scala +++ b/src/main/scala/com/tjclp/fastmcp/macros/PromptProcessor.scala @@ -27,11 +27,11 @@ private[macros] object PromptProcessor extends AnnotationProcessorBase: // 2️⃣ name / description with Scaladoc fallback ------------------------------------------ val (finalName, finalDesc) = nameAndDescription(promptAnnot, methodSym) - // 3️⃣ Collect @PromptParam metadata ------------------------------------------------------- + // 3️⃣ Collect @Param/@PromptParam metadata ------------------------------------------------------- val argExprs: List[Expr[PromptArgument]] = methodSym.paramSymss.headOption.getOrElse(Nil).map { pSym => val (descOpt, required) = MacroUtils.parsePromptParamArgs( - MacroUtils.extractAnnotation[PromptParam](pSym) + MacroUtils.extractParamAnnotation(pSym, Some("Prompt")) ) '{ PromptArgument(${ Expr(pSym.name) }, ${ Expr(descOpt) }, ${ Expr(required) }) } } diff --git a/src/main/scala/com/tjclp/fastmcp/macros/ResourceProcessor.scala b/src/main/scala/com/tjclp/fastmcp/macros/ResourceProcessor.scala index aca5a77..5c995d8 100644 --- a/src/main/scala/com/tjclp/fastmcp/macros/ResourceProcessor.scala +++ b/src/main/scala/com/tjclp/fastmcp/macros/ResourceProcessor.scala @@ -57,9 +57,9 @@ private[macros] object ResourceProcessor extends AnnotationProcessorBase: if !isTemplate then '{ None } else val list = paramSyms.map { pSym => - // Extract @ResourceParam description / required + // Extract @Param/@ResourceParam description / required val (descOpt, required) = - MacroUtils.extractAnnotation[ResourceParam](pSym) match + MacroUtils.extractParamAnnotation(pSym, Some("Resource")) match case Some(annotTerm) => var d: Option[String] = None var req: Boolean = true diff --git a/src/main/scala/com/tjclp/fastmcp/macros/ToolProcessor.scala b/src/main/scala/com/tjclp/fastmcp/macros/ToolProcessor.scala index 08dca5a..cb920a6 100644 --- a/src/main/scala/com/tjclp/fastmcp/macros/ToolProcessor.scala +++ b/src/main/scala/com/tjclp/fastmcp/macros/ToolProcessor.scala @@ -60,13 +60,13 @@ private[macros] object ToolProcessor extends AnnotationProcessorBase: ) } - // Collect @ToolParam descriptions so we can inject them into the schema + // Collect @Param/@ToolParam descriptions so we can inject them into the schema val paramDescriptions: Map[String, String] = methodSym.paramSymss.headOption .getOrElse(Nil) .flatMap { pSym => MacroUtils - .extractAnnotation[ToolParam](pSym) + .extractParamAnnotation(pSym, Some("Tool")) .flatMap { annotTerm => // description is either the first String literal or the named arg "description" annotTerm match