diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index b0b1be1..2d997af 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" authors = ["Ballerina"] keywords = ["mcp"] repository = "https://github.com/ballerina-platform/module-ballerina-mcp" @@ -15,5 +15,5 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.stdlib." artifactId = "mcp-native" -version = "1.0.2" -path = "../native/build/libs/mcp-native-1.0.2.jar" +version = "1.0.3" +path = "../native/build/libs/mcp-native-1.0.3-SNAPSHOT.jar" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 0a16075..fef8a59 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,7 +3,7 @@ id = "mcp-compiler-plugin" class = "io.ballerina.stdlib.mcp.plugin.McpCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/mcp-compiler-plugin-1.0.2.jar" +path = "../compiler-plugin/build/libs/mcp-compiler-plugin-1.0.3-SNAPSHOT.jar" [[dependency]] path = "../compiler-plugin/build/libs/ballerina-to-openapi-2.3.0.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index cca6aa3..ecad72e 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -229,7 +229,7 @@ dependencies = [ [[package]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina/types.bal b/ballerina/types.bal index 582743d..0dc17cb 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -60,14 +60,17 @@ public type ProgressToken string|int; # An opaque token used to represent a cursor for pagination. public type Cursor string; +# Optional metadata for requests +public type Meta record { + # If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). + # The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + ProgressToken progressToken?; +}; + # Parameters for the request public type RequestParams record { - # Optional parameters for the request - record { - # If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). - # The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - ProgressToken progressToken?; - } _meta?; + # Optional metadata for the request + Meta _meta?; }; # Represents a generic request in the protocol @@ -315,6 +318,7 @@ public type ListToolsResult record { # The server's response to a tool call. public type CallToolResult record { + *Result; # The content of the tool call result (TextContent|ImageContent|AudioContent|EmbeddedResource)[] content; # Whether the tool call ended in an error. diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/SchemaUtils.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/SchemaUtils.java index 0000283..ed8a1b5 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/SchemaUtils.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/SchemaUtils.java @@ -37,6 +37,7 @@ import java.util.stream.Collectors; import static io.ballerina.stdlib.mcp.plugin.RemoteFunctionAnalysisTask.EMPTY_STRING; +import static io.ballerina.stdlib.mcp.plugin.Utils.isMetaParameter; import static io.ballerina.stdlib.mcp.plugin.Utils.isSessionType; /** @@ -61,7 +62,9 @@ public static String getParameterSchema(FunctionSymbol functionSymbol, SyntaxNod TypeMapper typeMapper = new TypeMapperImpl(context); for (ParameterSymbol parameterSymbol : parameterSymbolList) { try { - if (isSessionType(parameterSymbol.typeDescriptor())) { + // Skip Session and Meta parameters - they are not part of the tool schema + if (isSessionType(parameterSymbol.typeDescriptor()) || + isMetaParameter(parameterSymbol.typeDescriptor())) { continue; } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/Utils.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/Utils.java index 7c39782..692f819 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/Utils.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/Utils.java @@ -28,7 +28,9 @@ import io.ballerina.compiler.api.symbols.ServiceDeclarationSymbol; import io.ballerina.compiler.api.symbols.Symbol; import io.ballerina.compiler.api.symbols.SymbolKind; +import io.ballerina.compiler.api.symbols.TypeDescKind; import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.compiler.api.symbols.UnionTypeSymbol; import io.ballerina.compiler.api.values.ConstantValue; import io.ballerina.compiler.syntax.tree.AnnotationNode; import io.ballerina.compiler.syntax.tree.BasicLiteralNode; @@ -58,6 +60,7 @@ public class Utils { public static final String MCP_PACKAGE_NAME = "mcp"; public static final String MCP_BASIC_SERVICE_NAME = "Service"; public static final String SESSION_TYPE_NAME = "Session"; + public static final String META_TYPE_NAME = "Meta"; public static final String UNKNOWN_SYMBOL = "unknown"; public static final String SERVICE_CONFIG_ANNOTATION_NAME = "ServiceConfig"; public static final String SESSION_MODE_FIELD = "sessionMode"; @@ -190,6 +193,7 @@ public static boolean validateParameterTypes(FunctionSymbol functionSymbol, var parameterSymbolList = functionTypeSymbol.params().get(); boolean hasSessionParam = false; + boolean hasMetaParam = false; for (int i = 0; i < parameterSymbolList.size(); i++) { ParameterSymbol parameterSymbol = parameterSymbolList.get(i); @@ -197,6 +201,7 @@ public static boolean validateParameterTypes(FunctionSymbol functionSymbol, String parameterName = parameterSymbol.getName().orElse(UNKNOWN_SYMBOL); boolean isSessionType = isSessionType(parameterType); + boolean isMetaParam = isMetaParameter(parameterType); if (isSessionType) { if (hasSessionParam) { @@ -227,6 +232,36 @@ public static boolean validateParameterTypes(FunctionSymbol functionSymbol, } hasSessionParam = true; + } else if (isMetaParam) { + if (hasMetaParam) { + Diagnostic diagnostic = CompilationDiagnostic.getDiagnostic( + CompilationDiagnostic.META_PARAM_MUST_BE_LAST, + parameterSymbol.getLocation().orElse(alternativeLocation), + functionName, parameterName); + context.reportDiagnostic(diagnostic); + return false; + } + + if (i != parameterSymbolList.size() - 1) { + Diagnostic diagnostic = CompilationDiagnostic.getDiagnostic( + CompilationDiagnostic.META_PARAM_MUST_BE_LAST, + parameterSymbol.getLocation().orElse(alternativeLocation), + functionName, parameterName); + context.reportDiagnostic(diagnostic); + return false; + } + + // Check if Meta parameter is optional + if (!isOptionalType(parameterType)) { + Diagnostic diagnostic = CompilationDiagnostic.getDiagnostic( + CompilationDiagnostic.META_PARAM_MUST_BE_OPTIONAL, + parameterSymbol.getLocation().orElse(alternativeLocation), + functionName, parameterName); + context.reportDiagnostic(diagnostic); + return false; + } + + hasMetaParam = true; } else if (!isAnydataType(parameterType, context)) { Diagnostic diagnostic = CompilationDiagnostic.getDiagnostic( CompilationDiagnostic.INVALID_PARAMETER_TYPE, @@ -245,6 +280,58 @@ static boolean isSessionType(TypeSymbol typeSymbol) { && isMcpModuleSymbol(typeSymbol); } + static boolean isMetaType(TypeSymbol typeSymbol) { + return META_TYPE_NAME.equals(typeSymbol.getName().orElse("")) + && isMcpModuleSymbol(typeSymbol); + } + + /** + * Check if a TypeSymbol is optional/nullable (e.g., mcp:Meta?). + * An optional type is a union type that includes NIL as one of its members. + * + * @param typeSymbol The type symbol to check + * @return true if the type is optional/nullable, false otherwise + */ + static boolean isOptionalType(TypeSymbol typeSymbol) { + if (typeSymbol.typeKind() != TypeDescKind.UNION) { + return false; + } + + UnionTypeSymbol unionTypeSymbol = (UnionTypeSymbol) typeSymbol; + for (TypeSymbol memberType : unionTypeSymbol.memberTypeDescriptors()) { + if (memberType.typeKind() == TypeDescKind.NIL) { + return true; + } + } + return false; + } + + /** + * Check if a parameter is of Meta type (either mcp:Meta or mcp:Meta?). + * Returns true if the parameter is Meta type, and also indicates if it's optional. + * + * @param typeSymbol The type symbol to check + * @return true if the parameter is a Meta type (optional or not) + */ + static boolean isMetaParameter(TypeSymbol typeSymbol) { + // Direct Meta type check + if (isMetaType(typeSymbol)) { + return true; + } + + // Check if it's an optional Meta type (mcp:Meta?) + if (typeSymbol.typeKind() == TypeDescKind.UNION) { + UnionTypeSymbol unionTypeSymbol = (UnionTypeSymbol) typeSymbol; + for (TypeSymbol memberType : unionTypeSymbol.memberTypeDescriptors()) { + if (isMetaType(memberType)) { + return true; + } + } + } + + return false; + } + private static SessionMode getSessionMode(FunctionDefinitionNode functionDefinitionNode, SemanticModel semanticModel) { ServiceDeclarationNode serviceNode = (ServiceDeclarationNode) functionDefinitionNode.parent(); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/CompilationDiagnostic.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/CompilationDiagnostic.java index 15330d0..e4036c0 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/CompilationDiagnostic.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/CompilationDiagnostic.java @@ -33,7 +33,9 @@ public enum CompilationDiagnostic { UNABLE_TO_GENERATE_SCHEMA_FOR_FUNCTION(DiagnosticMessage.ERROR_101, DiagnosticCode.MCP_101, ERROR), INVALID_PARAMETER_TYPE(DiagnosticMessage.ERROR_102, DiagnosticCode.MCP_102, ERROR), SESSION_PARAM_MUST_BE_FIRST(DiagnosticMessage.ERROR_103, DiagnosticCode.MCP_103, ERROR), - SESSION_PARAM_NOT_ALLOWED_IN_STATELESS_MODE(DiagnosticMessage.ERROR_104, DiagnosticCode.MCP_104, ERROR); + SESSION_PARAM_NOT_ALLOWED_IN_STATELESS_MODE(DiagnosticMessage.ERROR_104, DiagnosticCode.MCP_104, ERROR), + META_PARAM_MUST_BE_LAST(DiagnosticMessage.ERROR_105, DiagnosticCode.MCP_105, ERROR), + META_PARAM_MUST_BE_OPTIONAL(DiagnosticMessage.ERROR_106, DiagnosticCode.MCP_106, ERROR); private final String diagnostic; private final String diagnosticCode; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/DiagnosticCode.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/DiagnosticCode.java index d8ea6d6..da73501 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/DiagnosticCode.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/DiagnosticCode.java @@ -25,5 +25,7 @@ public enum DiagnosticCode { MCP_101, MCP_102, MCP_103, - MCP_104 + MCP_104, + MCP_105, + MCP_106 } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/DiagnosticMessage.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/DiagnosticMessage.java index ea1f563..e9ad86e 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/DiagnosticMessage.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/mcp/plugin/diagnostics/DiagnosticMessage.java @@ -25,9 +25,12 @@ public enum DiagnosticMessage { ERROR_101("Failed to generate the parameter schema definition for the function ''{0}''." + " Specify the parameter schema manually using the `@mcp:McpTool` annotation's parameter field."), ERROR_102("Parameter ''{1}'' in function ''{0}'' must be of type 'anydata'. " + - "Only the first parameter can be of type 'mcp:Session'."), + "Only the first parameter can be of type 'mcp:Session' and only the last parameter can be of type " + + "'mcp:Meta'."), ERROR_103("Session parameter ''{1}'' in function ''{0}'' must be the first parameter."), - ERROR_104("Session parameter ''{1}'' in function ''{0}'' is not allowed when sessionMode is 'STATELESS'."); + ERROR_104("Session parameter ''{1}'' in function ''{0}'' is not allowed when sessionMode is 'STATELESS'."), + ERROR_105("Meta parameter ''{1}'' in function ''{0}'' must be the last parameter."), + ERROR_106("Meta parameter ''{1}'' in function ''{0}'' must be optional (e.g., 'mcp:Meta?')."); private final String message; diff --git a/examples/clients/mcp-crypto-client/Ballerina.toml b/examples/clients/mcp-crypto-client/Ballerina.toml index 8c3e013..029336f 100644 --- a/examples/clients/mcp-crypto-client/Ballerina.toml +++ b/examples/clients/mcp-crypto-client/Ballerina.toml @@ -10,5 +10,5 @@ observabilityIncluded = true [[dependency]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" repository = "local" diff --git a/examples/clients/mcp-crypto-client/Dependencies.toml b/examples/clients/mcp-crypto-client/Dependencies.toml index 5cdc13f..a08c4fe 100644 --- a/examples/clients/mcp-crypto-client/Dependencies.toml +++ b/examples/clients/mcp-crypto-client/Dependencies.toml @@ -219,10 +219,9 @@ modules = [ [[package]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" dependencies = [ {org = "ballerina", name = "http"}, - {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "uuid"} ] @@ -238,6 +237,7 @@ dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "log"}, {org = "ballerina", name = "mcp"}, + {org = "ballerina", name = "uuid"}, {org = "ballerinai", name = "observe"} ] modules = [ @@ -298,7 +298,7 @@ dependencies = [ [[package]] org = "ballerina" name = "time" -version = "2.7.0" +version = "2.8.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -306,7 +306,7 @@ dependencies = [ [[package]] org = "ballerina" name = "url" -version = "2.6.0" +version = "2.6.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -321,6 +321,9 @@ dependencies = [ {org = "ballerina", name = "lang.int"}, {org = "ballerina", name = "time"} ] +modules = [ + {org = "ballerina", packageName = "uuid", moduleName = "uuid"} +] [[package]] org = "ballerinai" diff --git a/examples/clients/mcp-crypto-client/main.bal b/examples/clients/mcp-crypto-client/main.bal index 97ae368..7ea65f4 100644 --- a/examples/clients/mcp-crypto-client/main.bal +++ b/examples/clients/mcp-crypto-client/main.bal @@ -17,6 +17,7 @@ import ballerina/io; import ballerina/log; import ballerina/mcp; +import ballerina/uuid; final mcp:StreamableHttpClient mcpClient = check new ("http://localhost:9091/mcp"); @@ -65,16 +66,30 @@ function demonstrateHashText() returns mcp:ClientError? { string testText = "Hello, MCP World!"; foreach string algorithm in algorithms { + // Generate a trace ID to track this request + string traceId = uuid:createType1AsString(); + mcp:CallToolResult result = check mcpClient->callTool({ name: "hashText", arguments: { "text": testText, "algorithm": algorithm + }, + _meta: { + "traceId": traceId } }); io:println(string `${algorithm.toUpperAscii()} Hash Result:`); io:println(result.toString()); + + if result._meta is record {} { + io:println("\nResponse Metadata:"); + record {} meta = result._meta; + foreach var [key, value] in meta.entries() { + io:println(string ` ${key}: ${value.toString()}`); + } + } io:println("---"); } @@ -88,6 +103,14 @@ function demonstrateHashText() returns mcp:ClientError? { io:println("Default Algorithm Hash Result:"); io:println(defaultResult.toString()); + + if defaultResult._meta is record {} { + io:println("\nResponse Metadata:"); + record {} meta = defaultResult._meta; + foreach var [key, value] in meta.entries() { + io:println(string ` ${key}: ${value.toString()}`); + } + } io:println("---"); } @@ -97,16 +120,29 @@ function demonstrateBase64Operations() returns mcp:ClientError? { string originalText = "Ballerina MCP is awesome!"; // Encode to Base64 + string traceId1 = uuid:createType1AsString(); mcp:CallToolResult encodeResult = check mcpClient->callTool({ name: "encodeBase64", arguments: { "text": originalText, "operation": "encode" + }, + _meta: { + "traceId": traceId1 } }); io:println("Base64 Encoding:"); io:println(encodeResult.toString()); + + + if encodeResult._meta is record {} { + io:println("\nResponse Metadata:"); + record {} meta = encodeResult._meta; + foreach var [key, value] in meta.entries() { + io:println(string ` ${key}: ${value.toString()}`); + } + } io:println("---"); // Test with default operation (encode) @@ -119,49 +155,40 @@ function demonstrateBase64Operations() returns mcp:ClientError? { io:println("Default Operation (Encode):"); io:println(defaultEncodeResult.toString()); + + if defaultEncodeResult._meta is record {} { + io:println("\nResponse Metadata:"); + record {} meta = defaultEncodeResult._meta; + foreach var [key, value] in meta.entries() { + io:println(string ` ${key}: ${value.toString()}`); + } + } io:println("---"); // For demonstration, let's decode a known Base64 string - string base64Text = "QmFsbGVyaW5hIE1DUCBBUE1hOSBhd2Vzb21lIQ=="; + string base64Text = "QmFsbGVyaW5hIE1DUCBpcyBhd2Vzb21lIQ=="; + string traceId2 = uuid:createType1AsString(); mcp:CallToolResult decodeResult = check mcpClient->callTool({ name: "encodeBase64", arguments: { "text": base64Text, "operation": "decode" + }, + _meta: { + "traceId": traceId2 } }); - io:println("Base64 Decoding:"); + io:println("\nBase64 Decoding:"); io:println(decodeResult.toString()); - io:println("---"); - // Test encoding and then decoding the same text - string testText = "Round trip test: Encode then decode"; - - mcp:CallToolResult roundTripEncode = check mcpClient->callTool({ - name: "encodeBase64", - arguments: { - "text": testText, - "operation": "encode" + if decodeResult._meta is record {} { + io:println("\nResponse Metadata:"); + record {} meta = decodeResult._meta; + foreach var [key, value] in meta.entries() { + io:println(string ` ${key}: ${value.toString()}`); } - }); - - io:println("Round Trip - Encode:"); - io:println(roundTripEncode.toString()); - - // Extract the encoded value (this is simplified for demo purposes) - string encodedValue = testText.toBytes().toBase64(); - - mcp:CallToolResult roundTripDecode = check mcpClient->callTool({ - name: "encodeBase64", - arguments: { - "text": encodedValue, - "operation": "decode" - } - }); - - io:println("Round Trip - Decode:"); - io:println(roundTripDecode.toString()); + } io:println("---"); } diff --git a/examples/clients/mcp-shopping-client/Ballerina.toml b/examples/clients/mcp-shopping-client/Ballerina.toml index 71a90b9..b33eec6 100644 --- a/examples/clients/mcp-shopping-client/Ballerina.toml +++ b/examples/clients/mcp-shopping-client/Ballerina.toml @@ -10,5 +10,5 @@ observabilityIncluded = true [[dependency]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" repository = "local" diff --git a/examples/clients/mcp-shopping-client/Dependencies.toml b/examples/clients/mcp-shopping-client/Dependencies.toml index a52ac62..84421aa 100644 --- a/examples/clients/mcp-shopping-client/Dependencies.toml +++ b/examples/clients/mcp-shopping-client/Dependencies.toml @@ -219,10 +219,9 @@ modules = [ [[package]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" dependencies = [ {org = "ballerina", name = "http"}, - {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "uuid"} ] @@ -298,7 +297,7 @@ dependencies = [ [[package]] org = "ballerina" name = "time" -version = "2.7.0" +version = "2.8.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -306,7 +305,7 @@ dependencies = [ [[package]] org = "ballerina" name = "url" -version = "2.6.0" +version = "2.6.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] diff --git a/examples/clients/mcp-weather-client/Ballerina.toml b/examples/clients/mcp-weather-client/Ballerina.toml index cc27695..21c5136 100644 --- a/examples/clients/mcp-weather-client/Ballerina.toml +++ b/examples/clients/mcp-weather-client/Ballerina.toml @@ -10,5 +10,5 @@ observabilityIncluded = true [[dependency]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" repository = "local" diff --git a/examples/clients/mcp-weather-client/Dependencies.toml b/examples/clients/mcp-weather-client/Dependencies.toml index 571fd76..ee1f964 100644 --- a/examples/clients/mcp-weather-client/Dependencies.toml +++ b/examples/clients/mcp-weather-client/Dependencies.toml @@ -219,10 +219,9 @@ modules = [ [[package]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" dependencies = [ {org = "ballerina", name = "http"}, - {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "uuid"} ] @@ -238,6 +237,7 @@ dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "log"}, {org = "ballerina", name = "mcp"}, + {org = "ballerina", name = "uuid"}, {org = "ballerinai", name = "observe"} ] modules = [ @@ -271,7 +271,7 @@ dependencies = [ [[package]] org = "ballerina" name = "observe" -version = "1.6.0" +version = "1.5.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -288,7 +288,7 @@ dependencies = [ [[package]] org = "ballerina" name = "task" -version = "2.11.0" +version = "2.10.0" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "time"}, @@ -306,7 +306,7 @@ dependencies = [ [[package]] org = "ballerina" name = "url" -version = "2.6.0" +version = "2.6.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -321,6 +321,9 @@ dependencies = [ {org = "ballerina", name = "lang.int"}, {org = "ballerina", name = "time"} ] +modules = [ + {org = "ballerina", packageName = "uuid", moduleName = "uuid"} +] [[package]] org = "ballerinai" diff --git a/examples/clients/mcp-weather-client/main.bal b/examples/clients/mcp-weather-client/main.bal index 2afad95..4af6c7f 100644 --- a/examples/clients/mcp-weather-client/main.bal +++ b/examples/clients/mcp-weather-client/main.bal @@ -17,6 +17,7 @@ import ballerina/io; import ballerina/log; import ballerina/mcp; +import ballerina/uuid; final mcp:StreamableHttpClient mcpClient = check new ("http://localhost:9090/mcp"); @@ -64,15 +65,29 @@ function demonstrateCurrentWeather() returns mcp:ClientError? { string[] cities = ["London", "New York", "Tokyo", "Sydney"]; foreach string city in cities { + // Generate a request ID to track this request + string requestId = uuid:createType1AsString(); + mcp:CallToolResult result = check mcpClient->callTool({ name: "getCurrentWeather", arguments: { "city": city + }, + _meta: { + "requestId": requestId } }); io:println(string `Current Weather for ${city}:`); io:println(result.toString()); + + if result._meta is record {} { + io:println("\nResponse Metadata:"); + record {} meta = result._meta; + foreach var [key, value] in meta.entries() { + io:println(string ` ${key}: ${value.toString()}`); + } + } io:println("---"); } } @@ -88,45 +103,30 @@ function demonstrateWeatherForecast() returns mcp:ClientError? { ]; foreach var testCase in testCases { + string requestId = uuid:createType1AsString(); + mcp:CallToolResult result = check mcpClient->callTool({ name: "getWeatherForecast", arguments: { "location": testCase.location, "days": testCase.days + }, + _meta: { + "requestId": requestId, + "forecastDays": testCase.days } }); io:println(string `${testCase.days}-Day Forecast for ${testCase.location}:`); io:println(result.toString()); - io:println("---"); - } - // Test edge cases - io:println("Testing edge cases:"); - - // Minimum forecast days - mcp:CallToolResult minResult = check mcpClient->callTool({ - name: "getWeatherForecast", - arguments: { - "location": "Amsterdam", - "days": 1 - } - }); - - io:println("1-Day Forecast for Amsterdam:"); - io:println(minResult.toString()); - io:println("---"); - - // Maximum forecast days - mcp:CallToolResult maxResult = check mcpClient->callTool({ - name: "getWeatherForecast", - arguments: { - "location": "Rome", - "days": 7 + if result._meta is record {} { + io:println("\nResponse Metadata:"); + record {} meta = result._meta; + foreach var [key, value] in meta.entries() { + io:println(string ` ${key}: ${value.toString()}`); + } } - }); - - io:println("7-Day Forecast for Rome:"); - io:println(maxResult.toString()); - io:println("---"); + io:println("---"); + } } diff --git a/examples/servers/mcp-crypto-server/Ballerina.toml b/examples/servers/mcp-crypto-server/Ballerina.toml index 0a95703..2f07da3 100644 --- a/examples/servers/mcp-crypto-server/Ballerina.toml +++ b/examples/servers/mcp-crypto-server/Ballerina.toml @@ -10,5 +10,5 @@ observabilityIncluded = true [[dependency]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" repository = "local" diff --git a/examples/servers/mcp-crypto-server/Dependencies.toml b/examples/servers/mcp-crypto-server/Dependencies.toml index 7497903..f18c427 100644 --- a/examples/servers/mcp-crypto-server/Dependencies.toml +++ b/examples/servers/mcp-crypto-server/Dependencies.toml @@ -222,10 +222,9 @@ modules = [ [[package]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" dependencies = [ {org = "ballerina", name = "http"}, - {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "uuid"} ] @@ -242,6 +241,7 @@ dependencies = [ {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "log"}, {org = "ballerina", name = "mcp"}, + {org = "ballerina", name = "time"}, {org = "ballerinai", name = "observe"} ] modules = [ @@ -302,15 +302,18 @@ dependencies = [ [[package]] org = "ballerina" name = "time" -version = "2.7.0" +version = "2.8.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] +modules = [ + {org = "ballerina", packageName = "time", moduleName = "time"} +] [[package]] org = "ballerina" name = "url" -version = "2.6.0" +version = "2.6.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] diff --git a/examples/servers/mcp-crypto-server/main.bal b/examples/servers/mcp-crypto-server/main.bal index cf45aa6..964f3fd 100644 --- a/examples/servers/mcp-crypto-server/main.bal +++ b/examples/servers/mcp-crypto-server/main.bal @@ -18,6 +18,7 @@ import ballerina/crypto; import ballerina/lang.array; import ballerina/log; import ballerina/mcp; +import ballerina/time; listener mcp:Listener mcpListener = check new (9091); @@ -79,12 +80,20 @@ service mcp:AdvancedService /mcp on mcpListener { remote isolated function onCallTool(mcp:CallToolParams params, mcp:Session? session) returns mcp:CallToolResult|mcp:ServerError { record {} arguments = params.arguments ?: {}; + + // Extract metadata from client request if present + record {} clientMeta = {}; + if params._meta is record {} { + clientMeta = params._meta; + log:printInfo(string `Received _meta from client: ${clientMeta.toJsonString()}`); + } + match params.name { "hashText" => { - return self.handleHashText(arguments); + return self.handleHashText(arguments, clientMeta); } "encodeBase64" => { - return self.handleBase64(arguments); + return self.handleBase64(arguments, clientMeta); } _ => { return error mcp:ServerError(string `Unknown tool: ${params.name}`); @@ -92,7 +101,9 @@ service mcp:AdvancedService /mcp on mcpListener { } } - private isolated function handleHashText(record {} arguments) returns mcp:CallToolResult|mcp:ServerError { + private isolated function handleHashText(record {} arguments, record {} clientMeta) returns mcp:CallToolResult|mcp:ServerError { + time:Utc startTime = time:utcNow(); + string|error text = (arguments["text"]).cloneWithType(); if text is error { return error mcp:ServerError("Invalid 'text' parameter"); @@ -134,6 +145,9 @@ service mcp:AdvancedService /mcp on mcpListener { } } + time:Utc endTime = time:utcNow(); + decimal executionTime = time:utcDiffSeconds(endTime, startTime) * 1000; + log:printInfo(string `Hashed text using ${algorithm}: ${text} -> ${hashedValue}`); HashResult result = { @@ -142,17 +156,29 @@ service mcp:AdvancedService /mcp on mcpListener { originalText: text }; + record {} responseMeta = { + "executionTimeMs": executionTime, + "inputLength": text.length(), + "outputLength": hashedValue.length() + }; + foreach var [key, value] in clientMeta.entries() { + responseMeta[key] = value; + } + return { content: [ { 'type: "text", text: result.toJsonString() } - ] + ], + _meta: responseMeta }; } - private isolated function handleBase64(record {} arguments) returns mcp:CallToolResult|mcp:ServerError { + private isolated function handleBase64(record {} arguments, record {} clientMeta) returns mcp:CallToolResult|mcp:ServerError { + time:Utc startTime = time:utcNow(); + string|error text = (arguments["text"]).cloneWithType(); if text is error { return error mcp:ServerError("Invalid 'text' parameter"); @@ -184,6 +210,9 @@ service mcp:AdvancedService /mcp on mcpListener { return error mcp:ServerError("Invalid operation. Use 'encode' or 'decode'"); } + time:Utc endTime = time:utcNow(); + decimal executionTime = time:utcDiffSeconds(endTime, startTime) * 1000; + log:printInfo(string `Base64 ${operation}: ${text} -> ${resultValue}`); Base64Result result = { @@ -192,13 +221,23 @@ service mcp:AdvancedService /mcp on mcpListener { originalInput: text }; + record {} responseMeta = { + "executionTimeMs": executionTime, + "inputLength": text.length(), + "outputLength": resultValue.length() + }; + foreach var [key, value] in clientMeta.entries() { + responseMeta[key] = value; + } + return { content: [ { 'type: "text", text: result.toJsonString() } - ] + ], + _meta: responseMeta }; } } diff --git a/examples/servers/mcp-shopping-server/Ballerina.toml b/examples/servers/mcp-shopping-server/Ballerina.toml index 24fe642..317c230 100644 --- a/examples/servers/mcp-shopping-server/Ballerina.toml +++ b/examples/servers/mcp-shopping-server/Ballerina.toml @@ -10,5 +10,5 @@ observabilityIncluded = true [[dependency]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" repository = "local" diff --git a/examples/servers/mcp-shopping-server/Dependencies.toml b/examples/servers/mcp-shopping-server/Dependencies.toml index 1d3c3a5..b6daf53 100644 --- a/examples/servers/mcp-shopping-server/Dependencies.toml +++ b/examples/servers/mcp-shopping-server/Dependencies.toml @@ -216,10 +216,9 @@ modules = [ [[package]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" dependencies = [ {org = "ballerina", name = "http"}, - {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "uuid"} ] @@ -295,7 +294,7 @@ dependencies = [ [[package]] org = "ballerina" name = "time" -version = "2.7.0" +version = "2.8.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -306,7 +305,7 @@ modules = [ [[package]] org = "ballerina" name = "url" -version = "2.6.0" +version = "2.6.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] diff --git a/examples/servers/mcp-weather-server/Ballerina.toml b/examples/servers/mcp-weather-server/Ballerina.toml index dd7f9e8..0dacb41 100644 --- a/examples/servers/mcp-weather-server/Ballerina.toml +++ b/examples/servers/mcp-weather-server/Ballerina.toml @@ -10,5 +10,5 @@ observabilityIncluded = true [[dependency]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" repository = "local" diff --git a/examples/servers/mcp-weather-server/Dependencies.toml b/examples/servers/mcp-weather-server/Dependencies.toml index b7835a0..b121089 100644 --- a/examples/servers/mcp-weather-server/Dependencies.toml +++ b/examples/servers/mcp-weather-server/Dependencies.toml @@ -216,10 +216,9 @@ modules = [ [[package]] org = "ballerina" name = "mcp" -version = "1.0.2" +version = "1.0.3" dependencies = [ {org = "ballerina", name = "http"}, - {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "uuid"} ] @@ -269,7 +268,7 @@ dependencies = [ [[package]] org = "ballerina" name = "observe" -version = "1.6.0" +version = "1.5.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -298,7 +297,7 @@ modules = [ [[package]] org = "ballerina" name = "task" -version = "2.11.0" +version = "2.10.0" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "time"}, @@ -308,7 +307,7 @@ dependencies = [ [[package]] org = "ballerina" name = "time" -version = "2.7.0" +version = "2.8.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -319,7 +318,7 @@ modules = [ [[package]] org = "ballerina" name = "url" -version = "2.6.0" +version = "2.6.1" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] diff --git a/examples/servers/mcp-weather-server/main.bal b/examples/servers/mcp-weather-server/main.bal index b7a7d1d..c2dd4d3 100644 --- a/examples/servers/mcp-weather-server/main.bal +++ b/examples/servers/mcp-weather-server/main.bal @@ -36,7 +36,12 @@ service mcp:Service /mcp on mcpListener { - location (string, required): City name or coordinates (e.g., "London", "40.7128,-74.0060") ` } - remote function getCurrentWeather(string city) returns Weather|error { + remote function getCurrentWeather(string city, mcp:Meta? meta) returns Weather|error { + // Log received metadata if present + if meta is mcp:Meta { + log:printInfo(string `Received _meta from client: ${meta.toJsonString()}`); + } + log:printInfo(string `Getting current weather for: ${city}`); // Generate random weather data @@ -67,8 +72,14 @@ service mcp:Service /mcp on mcpListener { # # + location - City name or coordinates (e.g., "London", "40.7128,-74.0060") # + days - Number of days to forecast (1-7) + # + meta - Optional metadata for the request # + return - Weather forecast for the specified location and days - remote function getWeatherForecast(string location, int days) returns WeatherForecast|error { + remote function getWeatherForecast(string location, int days, mcp:Meta? meta) returns WeatherForecast|error { + // Log received metadata if present + if meta is mcp:Meta { + log:printInfo(string `Received _meta from client: ${meta.toJsonString()}`); + } + log:printInfo(string `Getting ${days}-day weather forecast for: ${location}`); // Generate forecast items with random data diff --git a/native/src/main/java/io/ballerina/stdlib/mcp/McpServiceMethodHelper.java b/native/src/main/java/io/ballerina/stdlib/mcp/McpServiceMethodHelper.java index 0a28374..2eebdff 100644 --- a/native/src/main/java/io/ballerina/stdlib/mcp/McpServiceMethodHelper.java +++ b/native/src/main/java/io/ballerina/stdlib/mcp/McpServiceMethodHelper.java @@ -63,9 +63,11 @@ public final class McpServiceMethodHelper { private static final String TEXT_VALUE_NAME = "text"; private static final String MCP_SERVICE_FIELD = "mcpService"; - // MCP Session-related constants + // MCP Session and Meta-related constants private static final String MCP_PACKAGE_NAME = "mcp"; private static final String SESSION_TYPE_NAME = "Session"; + private static final String META_TYPE_NAME = "Meta"; + private static final String META_FIELD_NAME = "_meta"; private McpServiceMethodHelper() {} @@ -140,8 +142,12 @@ public static Object callToolForRemoteFunctions(Environment env, BObject mcpServ .createError("RemoteMethodType with name '" + toolName.getValue() + "' not found"); } + // Extract metadata from params + Object meta = params.get(fromString(META_FIELD_NAME)); + Object argsOrError = - buildArgsForMethod(method.get(), (BMap) params.get(fromString(ARGUMENTS_FIELD_NAME)), session); + buildArgsForMethod(method.get(), (BMap) params.get(fromString(ARGUMENTS_FIELD_NAME)), + session, meta); if (argsOrError instanceof BError) { return argsOrError; @@ -150,7 +156,7 @@ public static Object callToolForRemoteFunctions(Environment env, BObject mcpServ Object[] args = (Object[]) argsOrError; Object result = env.getRuntime().callMethod(mcpService, toolName.getValue(), null, args); - return createCallToolResult(typed, result); + return createCallToolResult(typed, result, meta); } /** @@ -203,7 +209,8 @@ private static BMap createToolRecord(ArrayType toolsArrayType, return tool; } - private static Object buildArgsForMethod(RemoteMethodType method, BMap arguments, Object session) { + private static Object buildArgsForMethod(RemoteMethodType method, BMap arguments, Object session, + Object meta) { List params = List.of(method.getParameters()); Object[] args = new Object[params.size()]; for (int i = 0; i < params.size(); i++) { @@ -211,6 +218,8 @@ private static Object buildArgsForMethod(RemoteMethodType method, BMap arg if (isSessionParameter(param)) { args[i] = session; + } else if (isMetaParameter(param)) { + args[i] = meta; } else { String paramName = param.name; Object argValue = arguments == null ? null : arguments.get(fromString(paramName)); @@ -246,7 +255,28 @@ private static boolean isSessionParameter(Parameter param) { && SESSION_TYPE_NAME.equals(paramType.getName()); } - private static Object createCallToolResult(BTypedesc typed, Object result) { + private static boolean isMetaParameter(Parameter param) { + Type paramType = param.type; + + // Direct Meta type check + if (paramType.getPackage() != null + && MCP_PACKAGE_NAME.equals(paramType.getPackage().getName()) + && META_TYPE_NAME.equals(paramType.getName())) { + return true; + } + + // Check if it's an optional Meta type (mcp:Meta?) + if (paramType instanceof UnionType unionType) { + return unionType.getMemberTypes().stream() + .anyMatch(type -> type.getPackage() != null + && MCP_PACKAGE_NAME.equals(type.getPackage().getName()) + && META_TYPE_NAME.equals(type.getName())); + } + + return false; + } + + private static Object createCallToolResult(BTypedesc typed, Object result, Object meta) { RecordType resultRecordType = (RecordType) typed.getDescribingType(); BMap callToolResult = ValueCreator.createRecordValue(resultRecordType); @@ -268,6 +298,12 @@ private static Object createCallToolResult(BTypedesc typed, Object result) { contentArray.append(textContent); callToolResult.put(fromString(CONTENT_FIELD_NAME), contentArray); + + // Attach modified metadata to response if present + if (meta != null) { + callToolResult.put(fromString(META_FIELD_NAME), meta); + } + return callToolResult; } }