diff --git a/src/main/scala/com/tjclp/fastmcp/runtime/RefResolver.scala b/src/main/scala/com/tjclp/fastmcp/runtime/RefResolver.scala index 607fad6..ebbafe0 100644 --- a/src/main/scala/com/tjclp/fastmcp/runtime/RefResolver.scala +++ b/src/main/scala/com/tjclp/fastmcp/runtime/RefResolver.scala @@ -1,9 +1,5 @@ package com.tjclp.fastmcp.runtime -import java.lang.invoke.MethodHandles -import java.lang.invoke.MethodType -import scala.jdk.CollectionConverters.* - /** Runtime utility for resolving function references and invoking methods. This class is extracted * from the macro utilities to allow for runtime resolution without requiring macro expansion. */ @@ -20,6 +16,8 @@ object RefResolver: * The result of the function invocation * @throws IllegalArgumentException * if the function cannot be invoked with the given arguments + * @throws IllegalArgumentException + * if more than 22 arguments are provided (Scala function limitation) */ def invokeFunctionWithArgs(fun: Any, args: List[Any]): Any = (args.length, fun) match @@ -28,12 +26,464 @@ object RefResolver: case (2, f: Function2[?, ?, ?]) => f.asInstanceOf[Function2[Any, Any, Any]](args(0), args(1)) case (3, f: Function3[?, ?, ?, ?]) => f.asInstanceOf[Function3[Any, Any, Any, Any]](args(0), args(1), args(2)) - case _ => // universal fallback - val handle = MethodHandles - .lookup() - .findVirtual( - fun.getClass, - "apply", - MethodType.genericMethodType(args.length) - ) - handle.invokeWithArguments((fun :: args).map(_.asInstanceOf[Object]).asJava) + case (4, f: Function4[?, ?, ?, ?, ?]) => + f.asInstanceOf[Function4[Any, Any, Any, Any, Any]](args(0), args(1), args(2), args(3)) + case (5, f: Function5[?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function5[Any, Any, Any, Any, Any, Any]]( + args(0), + args(1), + args(2), + args(3), + args(4) + ) + case (6, f: Function6[?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function6[Any, Any, Any, Any, Any, Any, Any]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5) + ) + case (7, f: Function7[?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function7[Any, Any, Any, Any, Any, Any, Any, Any]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6) + ) + case (8, f: Function8[?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function8[Any, Any, Any, Any, Any, Any, Any, Any, Any]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7) + ) + case (9, f: Function9[?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function9[Any, Any, Any, Any, Any, Any, Any, Any, Any, Any]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8) + ) + case (10, f: Function10[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function10[Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9) + ) + case (11, f: Function11[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function11[Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10) + ) + case (12, f: Function12[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function12[Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11) + ) + case (13, f: Function13[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[ + Function13[Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any] + ]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12) + ) + case (14, f: Function14[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[ + Function14[Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any] + ]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13) + ) + case (15, f: Function15[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[ + Function15[Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any] + ]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13), + args(14) + ) + case (16, f: Function16[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function16[ + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any + ]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13), + args(14), + args(15) + ) + case (17, f: Function17[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function17[ + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any + ]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13), + args(14), + args(15), + args(16) + ) + case (18, f: Function18[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function18[ + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any + ]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13), + args(14), + args(15), + args(16), + args(17) + ) + case (19, f: Function19[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function19[ + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any + ]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13), + args(14), + args(15), + args(16), + args(17), + args(18) + ) + case (20, f: Function20[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function20[ + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any + ]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13), + args(14), + args(15), + args(16), + args(17), + args(18), + args(19) + ) + case (21, f: Function21[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?]) => + f.asInstanceOf[Function21[ + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any + ]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13), + args(14), + args(15), + args(16), + args(17), + args(18), + args(19), + args(20) + ) + case ( + 22, + f: Function22[?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?] + ) => + f.asInstanceOf[Function22[ + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any + ]]( + args(0), + args(1), + args(2), + args(3), + args(4), + args(5), + args(6), + args(7), + args(8), + args(9), + args(10), + args(11), + args(12), + args(13), + args(14), + args(15), + args(16), + args(17), + args(18), + args(19), + args(20), + args(21) + ) + case _ => // Unsupported, but you shouldn't do this anyway... + throw new IllegalArgumentException( + s"Function invocation with ${args.length} arguments is not supported. " + + "FastMCP's RefResolver is limited to a maximum of 22 arguments per function. " + + "Consider simplifying your implementation." + ) diff --git a/src/main/scala/com/tjclp/fastmcp/server/FastMcpServer.scala b/src/main/scala/com/tjclp/fastmcp/server/FastMcpServer.scala index d4d0e7d..e2b7f06 100644 --- a/src/main/scala/com/tjclp/fastmcp/server/FastMcpServer.scala +++ b/src/main/scala/com/tjclp/fastmcp/server/FastMcpServer.scala @@ -5,6 +5,7 @@ import io.modelcontextprotocol.server.McpAsyncServer import io.modelcontextprotocol.server.McpAsyncServerExchange import io.modelcontextprotocol.server.McpServer import io.modelcontextprotocol.server.McpServerFeatures +import io.modelcontextprotocol.server.transport.StdioServerTransportProvider import io.modelcontextprotocol.spec.McpSchema import io.modelcontextprotocol.spec.McpServerTransportProvider import reactor.core.publisher.Mono @@ -15,8 +16,8 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.jdk.CollectionConverters.* import scala.util.Failure import scala.util.Success + import core.* -import io.modelcontextprotocol.server.transport.StdioServerTransportProvider import server.manager.* // Needed for runToFuture onComplete /** Main server class for FastMCP-Scala diff --git a/src/test/scala/com/tjclp/fastmcp/macros/StringPromptTest.scala b/src/test/scala/com/tjclp/fastmcp/macros/StringPromptTest.scala index 9b3340c..62a8edb 100644 --- a/src/test/scala/com/tjclp/fastmcp/macros/StringPromptTest.scala +++ b/src/test/scala/com/tjclp/fastmcp/macros/StringPromptTest.scala @@ -1,8 +1,8 @@ package com.tjclp.fastmcp.macros import com.tjclp.fastmcp.core.* -import com.tjclp.fastmcp.server.FastMcpServer import com.tjclp.fastmcp.macros.RegistrationMacro.* +import com.tjclp.fastmcp.server.FastMcpServer import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/src/test/scala/com/tjclp/fastmcp/runtime/RefResolverTest.scala b/src/test/scala/com/tjclp/fastmcp/runtime/RefResolverTest.scala new file mode 100644 index 0000000..0919987 --- /dev/null +++ b/src/test/scala/com/tjclp/fastmcp/runtime/RefResolverTest.scala @@ -0,0 +1,220 @@ +package com.tjclp.fastmcp.runtime + +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +/** Regression tests for RefResolver to ensure it correctly handles functions with various numbers + * of arguments. Prior to the fix in commit 61d3392, functions with more than 3 arguments would + * fail with a "symbolic reference class is not accessible" error. + * + * Note: Standard Scala Function types support up to 22 arguments (Function0 to Function22). Scala + * 3 adds support for arbitrary function arity using Tuples via extension methods, but + * RefResolver's direct pattern matching is limited to 22 arguments. + */ +class RefResolverTest extends AnyFunSuite with Matchers { + + // Test function with exactly 4 arguments + test("invokeFunctionWithArgs should correctly invoke a function with 4 arguments") { + def fourArgsFunction(a: Int, b: String, c: Double, d: Boolean): String = + s"$a - $b - $c - $d" + + val result = RefResolver.invokeFunctionWithArgs( + fourArgsFunction, + List(42, "test", 3.14, true) + ) + + result should be("42 - test - 3.14 - true") + } + + // Testing with a lambda/anonymous function with 4 args + test("invokeFunctionWithArgs should correctly invoke a lambda function with 4 arguments") { + val fourArgsLambda = (a: Int, b: String, c: Double, d: Boolean) => s"Lambda: $a - $b - $c - $d" + + val result = RefResolver.invokeFunctionWithArgs( + fourArgsLambda, + List(42, "test", 3.14, true) + ) + + result should be("Lambda: 42 - test - 3.14 - true") + } + + // Test function with 5 arguments + test("invokeFunctionWithArgs should correctly invoke a function with 5 arguments") { + def fiveArgsFunction(a: Int, b: String, c: Double, d: Boolean, e: List[String]): String = + s"$a - $b - $c - $d - ${e.mkString(",")}" + + val result = RefResolver.invokeFunctionWithArgs( + fiveArgsFunction, + List(42, "test", 3.14, true, List("x", "y", "z")) + ) + + result should be("42 - test - 3.14 - true - x,y,z") + } + + // Test function with 10 arguments - testing a higher number in the middle range + test("invokeFunctionWithArgs should correctly invoke a function with 10 arguments") { + def tenArgsFunction( + a1: Int, + a2: Int, + a3: Int, + a4: Int, + a5: Int, + a6: Int, + a7: Int, + a8: Int, + a9: Int, + a10: Int + ): Int = + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + + val result = RefResolver.invokeFunctionWithArgs( + tenArgsFunction, + List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + ) + + result should be(55) // Sum of numbers 1 to 10 + } + + // Test function with maximum supported arguments (22) + test("invokeFunctionWithArgs should correctly invoke a function with 22 arguments") { + // Function that takes 22 arguments and joins them as strings + def maxArgsFunction( + a1: String, + a2: String, + a3: String, + a4: String, + a5: String, + a6: String, + a7: String, + a8: String, + a9: String, + a10: String, + a11: String, + a12: String, + a13: String, + a14: String, + a15: String, + a16: String, + a17: String, + a18: String, + a19: String, + a20: String, + a21: String, + a22: String + ): String = { + Seq( + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + a17, + a18, + a19, + a20, + a21, + a22 + ).mkString("-") + } + + val args = (1 to 22).map(_.toString).toList + + val result = RefResolver.invokeFunctionWithArgs( + maxArgsFunction, + args + ) + + result should be((1 to 22).map(_.toString).mkString("-")) + } + + // Test handling of symbolic references with 4+ arguments - regression test for the specific error mentioned + test("invokeFunctionWithArgs should correctly handle symbolic references with 4+ arguments") { + // This test targets the specific error: + // "symbolic reference class is not accessible: class com.tjclp.chat.mcp.VectorSearchMcpServer$$$Lambda$626/0x000000f8013844b0, from class com.tjclp.fastmcp.runtime.RefResolver$ (unnamed module @1b6d2f55)" + + // Create a class that contains a lambda with 4+ args to simulate a similar environment to VectorSearchMcpServer + class TestContainer { + // Lambda that will be created as an inner class (similar to the error case) + val fourArgSymbolicFunction = (a: Int, b: String, c: Double, d: List[Any]) => { + s"Symbolic-4Args: $a, $b, $c, ${d.mkString("[", ",", "]")}" + } + + // Lambda with 5 args + val fiveArgSymbolicFunction = + (a: Int, b: String, c: Double, d: List[Any], e: Map[String, Any]) => { + s"Symbolic-5Args: $a, $b, $c, ${d.mkString("[", ",", "]")}, ${e.mkString("{", ",", "}")}" + } + } + + val container = new TestContainer() + + // Test with 4 args + val result4 = RefResolver.invokeFunctionWithArgs( + container.fourArgSymbolicFunction, + List(42, "test", 3.14, List("a", "b", "c")) + ) + + result4 should be("Symbolic-4Args: 42, test, 3.14, [a,b,c]") + + // Test with 5 args + val result5 = RefResolver.invokeFunctionWithArgs( + container.fiveArgSymbolicFunction, + List(42, "test", 3.14, List("a", "b", "c"), Map("key" -> "value")) + ) + + result5 should be("Symbolic-5Args: 42, test, 3.14, [a,b,c], {key -> value}") + } + + // Test lambda with type Any - similar to how symbolic references might be created in real code + test( + "invokeFunctionWithArgs should correctly handle lambda with Any types with 4+ arguments" + ) { + // Lambda with Any types - this creates a similar situation to compiled symbolic references + val anyTypeLambda = (a: Any, b: Any, c: Any, d: Any) => { + s"AnyTypes: $a - $b - $c - $d" + } + + val result = RefResolver.invokeFunctionWithArgs( + anyTypeLambda, + List("val1", "val2", "val3", "val4") + ) + + result should be("AnyTypes: val1 - val2 - val3 - val4") + } + + // Test behavior with more than 22 arguments + test( + "invokeFunctionWithArgs should throw an exception for functions with more than 22 arguments" + ) { + // We cannot define a function that takes 23+ parameters directly in Scala + // So instead we'll use the generic fallback case in RefResolver by passing a function + // object that doesn't match the standard FunctionN classes + + // Create a test class that can process a list of arbitrary length + class ArbitraryArityFunction { + def apply(args: Any*): String = args.mkString(", ") + } + + val testFn = new ArbitraryArityFunction() + val args = (1 to 25).toList // More than the 22 arguments directly supported + + // This should throw an exception for exceeding the argument limit + val exception = intercept[IllegalArgumentException] { + RefResolver.invokeFunctionWithArgs(testFn, args) + } + + // Verify we get our custom error message + exception.getMessage should include("maximum of 22 arguments") + } +} diff --git a/src/test/scala/com/tjclp/fastmcp/server/FastMcpServerShutdownSpec.scala b/src/test/scala/com/tjclp/fastmcp/server/FastMcpServerShutdownSpec.scala index 35cf728..902e785 100644 --- a/src/test/scala/com/tjclp/fastmcp/server/FastMcpServerShutdownSpec.scala +++ b/src/test/scala/com/tjclp/fastmcp/server/FastMcpServerShutdownSpec.scala @@ -1,9 +1,10 @@ package com.tjclp.fastmcp.server -import java.util.concurrent.atomic.AtomicBoolean import zio.* import zio.test.* +import java.util.concurrent.atomic.AtomicBoolean + object FastMcpServerShutdownSpec extends ZIOSpecDefault { private final class MockCloseableServer(flag: AtomicBoolean) extends AutoCloseable {