From 9e0e543c2d24b1f82ceba1c4d8d94e407d16b6a5 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Tue, 27 May 2025 17:11:56 +0530 Subject: [PATCH 01/15] Implement initial version of MCP client --- .gitignore | 7 +- ballerina/Dependencies.toml | 254 +++++++++- ballerina/client.bal | 163 +++++++ ballerina/{main.bal => constants.bal} | 10 +- ballerina/error.bal | 42 ++ ballerina/protocol.bal | 12 + ballerina/server_msg_event_stream_gen.bal | 49 ++ ballerina/streamable_http.bal | 129 +++++ ballerina/types.bal | 557 ++++++++++++++++++++++ ballerina/utils.bal | 87 ++++ build.gradle | 1 + gradle.properties | 1 + 12 files changed, 1306 insertions(+), 6 deletions(-) create mode 100644 ballerina/client.bal rename ballerina/{main.bal => constants.bal} (71%) create mode 100644 ballerina/error.bal create mode 100644 ballerina/protocol.bal create mode 100644 ballerina/server_msg_event_stream_gen.bal create mode 100644 ballerina/streamable_http.bal create mode 100644 ballerina/types.bal create mode 100644 ballerina/utils.bal diff --git a/.gitignore b/.gitignore index ef94b2d..834a0a0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ .mtj.tmp/ # Package Files # +*.jar *.war *.nar *.ear @@ -32,7 +33,11 @@ target # MacOS *.DS_Store -.vscode/ +.classpath +.project +.settings +.vscode +.idea # Ballerina velocity.log* diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index d9cdb7f..94a6d89 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,100 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.12.0" +distribution-version = "2201.12.3" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.10.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "data.jsondata" +version = "1.1.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.14.1" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "data.jsondata"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] [[package]] org = "ballerina" @@ -24,6 +117,87 @@ org = "ballerina" name = "jballerina.java" version = "0.0.0" +[[package]] +org = "ballerina" +name = "jwt" +version = "2.15.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + [[package]] org = "ballerina" name = "lang.value" @@ -32,14 +206,92 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "log" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] + [[package]] org = "ballerina" name = "mcp" version = "0.1.0" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"} ] modules = [ {org = "ballerina", packageName = "mcp", moduleName = "mcp"} ] +[[package]] +org = "ballerina" +name = "mime" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + diff --git a/ballerina/client.bal b/ballerina/client.bal new file mode 100644 index 0000000..ff7434b --- /dev/null +++ b/ballerina/client.bal @@ -0,0 +1,163 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +# Client options for protocol initialization. +# +# + capabilities - Capabilities to advertise as being supported by this client. +public type ClientOptions record {| + *ProtocolOptions; + ClientCapabilities capabilities?; +|}; + +type ServerMessageHandler function (JSONRPCServerMessage serverMessage) returns error?; + +public distinct client class Client { + private final string url; + private StreamableHttpClientTransport? transport = (); + private ServerCapabilities? serverCapabilities = (); + private Implementation? serverVersion = (); + private final Implementation clientInfo; + private ClientCapabilities capabilities; + private int requestMessageId = 0; + + private ServerMessageHandler? serverMessageHandler = (); + + function init(string url, Implementation clientInfo, ClientOptions? options = ()) { + self.url = url; + self.clientInfo = clientInfo; + self.capabilities = options?.capabilities ?: {}; + } + + public function connect() returns error? { + self.transport = check new StreamableHttpClientTransport(self.url); + + StreamableHttpClientTransport? transport = self.transport; + if transport is StreamableHttpClientTransport { + string? sessionId = transport.getSessionId(); + // If sessionId is non-null, it means the server is trying to reconnect + if sessionId is string { + return; + } + + // If sessionId is null, it means the server is trying to establish a new connection + InitializeRequest initRequest = { + method: "initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: self.capabilities, + clientInfo: self.clientInfo + } + }; + JSONRPCServerMessage|stream|() response = check self.sendRequest(initRequest); + InitializeResult initResult = check handleInitializeResponse(response); + + if (!SUPPORTED_PROTOCOL_VERSIONS.some(v => v == initResult.protocolVersion)) { + return error ClientInitializationError("Server's protocol version is not supported: " + initResult.protocolVersion); + } + self.serverCapabilities = initResult.capabilities; + self.serverVersion = initResult.serverInfo; + + // Set the notification response handler + transport.setSseHandler(self.handleSseStream); + + // Send the initialized notification + InitializedNotification initNotification = { + method: "notifications/initialized" + }; + check self.sendNotification(initNotification); + } else { + return error TransportInitializationError("Failed to initialize transport for MCP client"); + } + } + + public function listTools() returns ListToolsResult|error { + ListToolsRequest listToolRequest = { + method: "tools/list" + }; + JSONRPCServerMessage|stream|() response = check self.sendRequest(listToolRequest); + ListToolsResult listToolResult = check handleListToolResult(response); + return listToolResult; + } + + public function callTool(CallToolParams params) returns JSONRPCServerMessage|stream|error { + CallToolRequest callToolRequest = { + method: "tools/call", + params: params + }; + JSONRPCServerMessage|stream|() response = check self.sendRequest(callToolRequest); + if response is () { + return error CallToolError("Received invalid response for tool call"); + } + return response; + } + + public function waitForCompletion() returns error? { + StreamableHttpClientTransport? transport = self.transport; + if transport is () { + return error UninitializedTransportError("Transport is not initialized for sending requests"); + } + + return transport.waitForCompletion(); + } + + public function setNotificationHandler(function (JSONRPCServerMessage) returns error? serverMessageHandler) returns error? { + self.serverMessageHandler = serverMessageHandler; + } + + private function sendRequest(Request request) returns JSONRPCServerMessage|stream|error? { + StreamableHttpClientTransport? transport = self.transport; + if transport is () { + return error UninitializedTransportError("Transport is not initialized for sending requests"); + } + + self.requestMessageId += 1; + JSONRPCRequest jsonrpcRequest = { + ...request, + jsonrpc: JSONRPC_VERSION, + id: self.requestMessageId + }; + + return transport.send(jsonrpcRequest); + } + + private function sendNotification(Notification notification) returns error? { + StreamableHttpClientTransport? transport = self.transport; + if transport is () { + return error UninitializedTransportError("Transport is not initialized for sending requests"); + } + + JSONRPCNotification jsonrpcNotification = { + ...notification, + jsonrpc: JSONRPC_VERSION + }; + + _ = check transport.send(jsonrpcNotification); + + } + + private function handleSseStream(stream 'stream) returns future { + worker SseWorker returns error? { + check from JSONRPCServerMessage serverMessage in 'stream + do { + ServerMessageHandler? handler = self.serverMessageHandler; + if handler is ServerMessageHandler { + check handler(serverMessage); + } + }; + } + return SseWorker; + } +} diff --git a/ballerina/main.bal b/ballerina/constants.bal similarity index 71% rename from ballerina/main.bal rename to ballerina/constants.bal index 1a6315e..5c1dd34 100644 --- a/ballerina/main.bal +++ b/ballerina/constants.bal @@ -14,7 +14,9 @@ // specific language governing permissions and limitations // under the License. -import ballerina/io; -public function main() { - io:println("This is MCP Library"); -} +// Transport related constants (headers) +const SESSION_ID_HEADER = "mcp-session-id"; +const ACCEPT_HEADER = "accept"; +const CONTENT_TYPE_HEADER = "content-type"; +const CONTENT_TYPE_JSON = "application/json"; +const CONTENT_TYPE_SSE = "text/event-stream"; diff --git a/ballerina/error.bal b/ballerina/error.bal new file mode 100644 index 0000000..2b0ba1d --- /dev/null +++ b/ballerina/error.bal @@ -0,0 +1,42 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +# Defines the common error type for the module. +public type Error distinct error; + +# Errors due to unsupported content type. +public type UnsupportedContentTypeError distinct Error; + +# Errors related to transport issues. +public type TransportError distinct Error; + +# Errors related to client. +public type ClientError distinct Error; + +# Errors related to uninitialized transport. +public type UninitializedTransportError distinct TransportError; + +# Errors related to initialization of the transport. +public type TransportInitializationError distinct TransportError; + +# Errors related to initialization of the client. +public type ClientInitializationError distinct ClientError; + +# Errors related to list tools operation. +public type ListToolError distinct ClientError; + +# Errors related to tool call operation. +public type CallToolError distinct ClientError; diff --git a/ballerina/protocol.bal b/ballerina/protocol.bal new file mode 100644 index 0000000..f1f70dd --- /dev/null +++ b/ballerina/protocol.bal @@ -0,0 +1,12 @@ +# Additional initialization options. +# +# + enforceStrictCapabilities - Whether to restrict emitted requests to only those that the remote +# side has indicated that they can handle, through their advertised +# capabilities. Note that this DOES NOT affect checking of _local_ +# side capabilities, as it is considered a logic error to mis-specify +# those. Currently this defaults to false, for backwards compatibility +# with SDK versions that did not advertise capabilities correctly. In +# future, this will default to true. +public type ProtocolOptions record {| + boolean enforceStrictCapabilities?; +|}; diff --git a/ballerina/server_msg_event_stream_gen.bal b/ballerina/server_msg_event_stream_gen.bal new file mode 100644 index 0000000..d3326ff --- /dev/null +++ b/ballerina/server_msg_event_stream_gen.bal @@ -0,0 +1,49 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +class ServerMessageEventStreamGenerator { + private stream eventStream; + + isolated function init(stream eventStream) returns error? { + self.eventStream = eventStream; + } + + public isolated function next() returns record {|JSONRPCServerMessage value;|}|error? { + record {|http:SseEvent value;|}? recordVal = check self.eventStream.next(); + + // If End of Stream + if recordVal is () { + return (); + } + + string? data = recordVal.value.data; + if data is () { + return error("Received SSE event without 'data' field in the event stream."); + } + + json jsonData = check data.fromJsonString(); + JSONRPCServerMessage rpcResponse = check jsonData.cloneWithType(); + return { + value: rpcResponse + }; + }; + + public isolated function close() returns error? { + check self.eventStream.close(); + } +} diff --git a/ballerina/streamable_http.bal b/ballerina/streamable_http.bal new file mode 100644 index 0000000..b85a7c3 --- /dev/null +++ b/ballerina/streamable_http.bal @@ -0,0 +1,129 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +# Configuration options for the StreamableHTTPClientTransport. +# +# + sessionId - Session ID for the connection. This is used to identify the session on the server. +# When not provided and connecting to a server that supports session IDs, the server will generate a new session ID. +public type StreamableHttpClientTransportOptions record {| + string? sessionId = (); +|}; + +type SseHandler function (stream 'stream) returns future; + +distinct class StreamableHttpClientTransport { + private final string url; + private final http:Client httpClient; + private string? sessionId; + + private SseHandler? streamHandler = (); + future? responseFuture = (); + + function init(string url, StreamableHttpClientTransportOptions? options = ()) returns error? { + self.url = url; + self.httpClient = check new (url); + self.sessionId = options?.sessionId; + } + + function send(JSONRPCMessage message) returns JSONRPCServerMessage|stream|error? { + map headers = self.commonHeaders(); + headers[CONTENT_TYPE_HEADER] = CONTENT_TYPE_JSON; + headers[ACCEPT_HEADER] = string `${CONTENT_TYPE_JSON}, ${CONTENT_TYPE_SSE}`; + + http:Response response = check self.httpClient->post("/", message, headers = headers); + + // Handle sessionId during the initialization request + string|error sessionId = response.getHeader(SESSION_ID_HEADER); + if sessionId is string { + self.sessionId = sessionId; + } + + // TODO: Handle Authorization + + // If the response is 202 Accepted, there's no body to process + if response.statusCode == 202 { + if (self.isInitializedNotification(message)) { + stream serverMsgEventStream = check self.startSse(); + SseHandler? streamHandler = self.streamHandler; + if streamHandler is SseHandler { + self.responseFuture = streamHandler(serverMsgEventStream); + } + } + return; + } + + // Handle the response based on the content type + string contentType = response.getContentType(); + if contentType.includes(CONTENT_TYPE_SSE) { + stream sseEventStream = check response.getSseEventStream(); + ServerMessageEventStreamGenerator serverMsgEventStreamGenerator = check new (sseEventStream); + stream serverMsgEventStream = new (serverMsgEventStreamGenerator); + return serverMsgEventStream; + } else if contentType.includes(CONTENT_TYPE_JSON) { + json jsonPayload = check response.getJsonPayload(); + JSONRPCServerMessage serverMsg = check jsonPayload.cloneWithType(); + return serverMsg; + } else { + return error UnsupportedContentTypeError("Unsupported content type: " + contentType); + } + } + + function getSessionId() returns string? { + return self.sessionId; + } + + function setSseHandler(SseHandler handler) { + self.streamHandler = handler; + } + + function waitForCompletion() returns error? { + future? responseFuture = self.responseFuture; + if (responseFuture is ()) { + return (); + } + return wait responseFuture; + } + + private function startSse() returns stream|error { + map headers = self.commonHeaders(); + headers[ACCEPT_HEADER] = "text/event-stream"; + + stream sseEventStream = check self.httpClient->get("/", headers = headers); + ServerMessageEventStreamGenerator serverMsgEventStreamGenerator = check new (sseEventStream); + stream serverMsgEventStream = new (serverMsgEventStreamGenerator); + return serverMsgEventStream; + } + + private function commonHeaders() returns map { + map headers = {}; + string? sessionId = self.sessionId; + if (sessionId is string) { + headers[SESSION_ID_HEADER] = sessionId; + } + return headers; + } + + private function isInitializedNotification(JSONRPCServerMessage message) returns boolean { + if message is JSONRPCNotification { + if message.method == "notifications/initialized" { + return true; + } + } + return false; + } +} diff --git a/ballerina/types.bal b/ballerina/types.bal new file mode 100644 index 0000000..d5931e2 --- /dev/null +++ b/ballerina/types.bal @@ -0,0 +1,557 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +# Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. +public type JSONRPCMessage JSONRPCRequest|JSONRPCNotification|JSONRPCResponse|JSONRPCError; + +# Refers to any valid JSON-RPC object that can be decoded off the wire. +public type JSONRPCServerMessage JSONRPCNotification|JSONRPCResponse|JSONRPCError; + +public const LATEST_PROTOCOL_VERSION = "2025-03-26"; +public const SUPPORTED_PROTOCOL_VERSIONS = [ + LATEST_PROTOCOL_VERSION, + "2024-11-05", + "2024-10-07" +]; + +public const JSONRPC_VERSION = "2.0"; + +# A progress token, used to associate progress notifications with the original request. +public type ProgressToken string|int; + +# An opaque token used to represent a cursor for pagination. +public type Cursor string; + +# Represents a generic request in the protocol +# +# + method - The method name for the request +# + params - Optional parameters for the request +public type Request record {| + string method; + record { + 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?; + } params?; +|}; + +# Represents a notification. +# +# + method - The method name of the notification +# + params - Additional parameters for the notification +public type Notification record {| + string method; + record { + record {} _meta?; + } params?; +|}; + +# Base result type with common fields. +# +# + _meta - This result property is reserved by the protocol to allow clients and servers +# to attach additional metadata to their responses. +public type Result record { + record {} _meta?; +}; + +# A uniquely identifying ID for a request in JSON-RPC. +public type RequestId string|int; + +# A request that expects a response. +# +# + jsonrpc - The JSON-RPC protocol version +# + id - Identifier established by the client that should be returned in the response +public type JSONRPCRequest record { + *Request; + JSONRPC_VERSION jsonrpc; + RequestId id; +}; + +# A notification which does not expect a response. +# +# + jsonrpc - The JSON-RPC protocol version +public type JSONRPCNotification record { + *Notification; + JSONRPC_VERSION jsonrpc; +}; + +# A successful (non-error) response to a request. +# +# + jsonrpc - The JSON-RPC protocol version +# + id - Identifier of the request +# + result - The result of the request +public type JSONRPCResponse record {| + JSONRPC_VERSION jsonrpc; + RequestId id; + ServerResult result; +|}; + +// Standard JSON-RPC error codes +public const PARSE_ERROR = -32700; +public const INVALID_REQUEST = -32600; +public const METHOD_NOT_FOUND = -32601; +public const INVALID_PARAMS = -32602; +public const INTERNAL_ERROR = -32603; + +# A response to a request that indicates an error occurred. +# +# + jsonrpc - The JSON-RPC protocol version +# + id - Identifier of the request +# + error - The error information +public type JSONRPCError record { + JSONRPC_VERSION jsonrpc; + RequestId id; + record { + # The error type that occurred + int code; + # A short description of the error. The message SHOULD be limited to a concise single sentence. + string message; + # Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + anydata data?; + } 'error; +}; + +# A response that indicates success but carries no data. +public type EmptyResult Result; + +# This notification can be sent by either side to indicate that it is cancelling a previously-issued request. +# +# The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification +# MAY arrive after the request has already finished. +# +# This notification indicates that the result will be unused, so any associated processing SHOULD cease. +# +# A client MUST NOT attempt to cancel its `initialize` request. +# +# + method - The method name for this notification +# + params - The parameters for the cancellation notification +public type CancelledNotification record {| + *Notification; + "notifications/cancelled" method; + record {| + # The ID of the request to cancel. + # + # This MUST correspond to the ID of a request previously issued in the same direction. + RequestId requestId; + # An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + string? reason = (); + |} params; +|}; + +# This request is sent from the client to the server when it first connects, asking it to begin initialization. +# +# + method - Method name for the request +# + params - Parameters for the initialize request +type InitializeRequest record {| + *Request; + "initialize" method; + record {| + # The latest version of the Model Context Protocol that the client supports. + # The client MAY decide to support older versions as well. + string protocolVersion; + # Capabilities supported by the client + ClientCapabilities capabilities; + # Information about the client implementation + Implementation clientInfo; + |} params; +|}; + +# After receiving an initialize request from the client, the server sends this response. +# +# + protocolVersion - The version of the Model Context Protocol that the server wants to use. +# This may not match the version that the client requested. +# If the client cannot support this version, it MUST disconnect. +# + capabilities - The capabilities of the server. +# + serverInfo - Information about the server implementation +# + instructions - Instructions describing how to use the server and its features. +# This can be used by clients to improve the LLM's understanding of available tools, resources, etc. +# It can be thought of like a "hint" to the model. +# For example, this information MAY be added to the system prompt. +public type InitializeResult record {| + *Result; + string protocolVersion; + ServerCapabilities capabilities; + Implementation serverInfo; + string instructions?; +|}; + +# This notification is sent from the client to the server after initialization has finished. +# +# + method - The method identifier for the notification, must be "notifications/initialized" +public type InitializedNotification record {| + *Notification; + "notifications/initialized" method; +|}; + +# Capabilities a client may support. Known capabilities are defined here, in this schema, +# but this is not a closed set: any client can define its own, additional capabilities. +# +# + experimental - Experimental, non-standard capabilities that the client supports. +# + roots - Present if the client supports listing roots. +# + sampling - Present if the client supports sampling from an LLM. +public type ClientCapabilities record { + record {|record {}...;|} experimental?; + record {| + # Whether the client supports notifications for changes to the roots list. + boolean listChanged?; + |} roots?; + record {} sampling?; +}; + +# Capabilities that a server may support. Known capabilities are defined here, in this schema, +# but this is not a closed set: any server can define its own, additional capabilities. +# +# + experimental - Experimental, non-standard capabilities that the server supports. +# + logging - Present if the server supports sending log messages to the client. +# + completions - Present if the server supports argument autocompletion suggestions. +# + prompts - Present if the server offers any prompt templates. +# + resources - Present if the server offers any resources to read. +# + tools - Present if the server offers any tools to call. +public type ServerCapabilities record { + record {|record {}...;|} experimental?; + record {} logging?; + record {} completions?; + record {| + # Whether this server supports notifications for changes to the prompt list. + boolean listChanged?; + |} prompts?; + record {| + # Whether this server supports subscribing to resource updates. + boolean subscribe?; + # Whether this server supports notifications for changes to the resource list. + boolean listChanged?; + |} resources?; + record {| + # Whether this server supports notifications for changes to the tool list. + boolean listChanged?; + |} tools?; +}; + +# Describes the name and version of an MCP implementation. +# +# + name - The name of the implementation +# + version - The version of the implementation +public type Implementation record {| + string name; + string version; +|}; + +# A ping, issued by either the server or the client, to check that +# the other party is still alive. The receiver must promptly respond, +# or else may be disconnected. +# +# + method - The method name +public type PingRequest record {| + *Request; + "ping" method; +|}; +// ProgressNotification +# Represents a paginated request with optional cursor-based pagination. +# +# + params - Optional pagination parameters +public type PaginatedRequest record {| + record {| + # An opaque token representing the current pagination position. + # If provided, the server should return results starting after this cursor. + Cursor cursor?; + |} params?; +|}; + +# Result that supports pagination +# +# + nextCursor - An opaque token representing the pagination position after the last returned result. +# If present, there may be more results available. +public type PaginatedResult record {| + *Result; + Cursor nextCursor?; +|}; +// ListResourcesRequest +// ListResourcesResult +// ListResourceTemplatesRequest +// ListResourceTemplatesResult +// ReadResourceRequest +// ReadResourceResult +// ResourceListChangedNotification +// SubscribeRequest +// UnsubscribeRequest +// ResourceUpdatedNotification +// Resource +// ResourceTemplate +# The contents of a specific resource or sub-resource. +# +# + uri - The URI of this resource. +# + mimeType - The MIME type of this resource, if known. +public type ResourceContents record {| + string uri; + string mimeType?; +|}; + +# Text resource contents +# +# + text - The text of the item. This must only be set if the item can actually be represented as text (not binary data). +public type TextResourceContents record {| + *ResourceContents; + string text; +|}; + +# Binary resource contents +# +# + blob - A base64-encoded string representing the binary data of the item. +public type BlobResourceContents record {| + *ResourceContents; + string blob; +|}; + +// ListPromptsRequest +// ListPromptsResult +// GetPromptRequest +// GetPromptResult +// Prompt +// PromptArgument +# The sender or recipient of messages and data in a conversation. +public type Role "user"|"assistant"; +// PromptMessage +# The contents of a resource, embedded into a prompt or tool call result. +# +# It is up to the client how best to render embedded resources for the benefit +# of the LLM and/or the user. +# +# + type - The type of content +# + resource - The resource content +# + annotations - Optional annotations for the client +public type EmbeddedResource record {| + "resource" 'type; + TextResourceContents|BlobResourceContents 'resource; + Annotations annotations?; +|}; +// PromptListChangedNotification +# Sent from the client to request a list of tools the server has. +# +# + method - The method identifier for this request +public type ListToolsRequest record {| + *PaginatedRequest; + "tools/list" method; +|}; + +# The server's response to a tools/list request from the client. +# +# + tools - A list of tools available on the server. +public type ListToolsResult record {| + *PaginatedResult; + Tool[] tools; +|}; + +# The server's response to a tool call. +# +# Any errors that originate from the tool SHOULD be reported inside the result +# object, with `isError` set to true, _not_ as an MCP protocol-level error +# response. Otherwise, the LLM would not be able to see that an error occurred +# and self-correct. +# +# However, any errors in _finding_ the tool, an error indicating that the +# server does not support tool calls, or any other exceptional conditions, +# should be reported as an MCP error response. +# +# + content - The content of the tool call result +# + isError - Whether the tool call ended in an error. +# If not set, this is assumed to be false (the call was successful). +public type CallToolResult record {| + (TextContent|ImageContent|AudioContent|EmbeddedResource)[] content; + boolean isError?; +|}; + +# Used by the client to invoke a tool provided by the server. +# +# + method - The JSON-RPC method name +# + params - The parameters for the tool call +public type CallToolRequest record {| + "tools/call" method; + CallToolParams params; +|}; + +# Parameters for the tools/call request +# +# + name - The name of the tool to invoke +# + arguments - Optional arguments to pass to the tool +public type CallToolParams record {| + string name; + record {} arguments?; +|}; +// ToolListChangedNotification +# Additional properties describing a Tool to clients. +# NOTE: all properties in ToolAnnotations are **hints**. +# They are not guaranteed to provide a faithful description of +# tool behavior (including descriptive properties like `title`). +# Clients should never make tool use decisions based on ToolAnnotations +# received from untrusted servers. +# +# + title - A human-readable title for the tool. +# + readOnlyHint - If true, the tool does not modify its environment. +# Default: false +# + destructiveHint - If true, the tool may perform destructive updates to its environment. +# If false, the tool performs only additive updates. +# (This property is meaningful only when `readOnlyHint == false`) +# Default: true +# + idempotentHint - If true, calling the tool repeatedly with the same arguments +# will have no additional effect on the its environment. +# (This property is meaningful only when `readOnlyHint == false`) +# Default: false +# + openWorldHint - If true, this tool may interact with an "open world" of external +# entities. If false, the tool's domain of interaction is closed. +# For example, the world of a web search tool is open, whereas that +# of a memory tool is not. +# Default: true +public type ToolAnnotations record {| + string title?; + boolean readOnlyHint?; + boolean destructiveHint?; + boolean idempotentHint?; + boolean openWorldHint?; +|}; + +# Definition for a tool the client can call. +# +# + name - The name of the tool +# + description - A human-readable description of the tool +# This can be used by clients to improve the LLM's understanding of available tools. +# + inputSchema - A JSON Schema object defining the expected parameters for the tool. +# + annotations - Optional additional tool information. +public type Tool record {| + string name; + string description?; + record { + "object" 'type; + record {|record {}...;|} properties?; + string[] required?; + } inputSchema; + ToolAnnotations annotations?; +|}; +// SetLevelRequest +// LoggingMessageNotification +// CreateMessageRequest +// CreateMessageResult +// SamplingMessage +# Optional annotations for the client. The client can use annotations to inform how objects are used or displayed +# +# + audience - Describes who the intended customer of this object or data is. +# This can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). +# + priority - Describes how important this data is for operating the server. +# A value of 1 means "most important," and indicates that the data is effectively required, +# while 0 means "least important," and indicates that the data is entirely optional. +public type Annotations record {| + Role[] audience?; + decimal priority?; +|}; + +# Text provided to or from an LLM. +# +# + type - The type of content +# + text - The text content of the message +# + annotations - Optional annotations for the client +public type TextContent record {| + "text" 'type; + string text; + Annotations annotations?; +|}; + +# An image provided to or from an LLM. +# +# + type - The type of content +# + data - The base64-encoded image data +# + mimeType - The MIME type of the image. Different providers may support different image types. +# + annotations - Optional annotations for the client +public type ImageContent record {| + "image" 'type; + string data; + string mimeType; + Annotations annotations?; +|}; + +# Audio provided to or from an LLM. +# +# + type - The type of content +# + data - The base64-encoded audio data +# + mimeType - The MIME type of the audio. Different providers may support different audio types. +# + annotations - Optional annotations for the client +public type AudioContent record {| + "audio" 'type; + string data; + string mimeType; + Annotations annotations?; +|}; +// ModelPreferences +// ModelHint +// CompleteRequest +// CompleteResult +// ResourceReference +// PromptReference +// ListRootsRequest +// ListRootsResult +// Root +// RootsListChangedNotification + +// Client messages +# Represents a request sent from the client to the server. +public type ClientRequest PingRequest | InitializeRequest | CallToolRequest | ListToolsRequest; +// | CompleteRequest +// | SetLevelRequest +// | GetPromptRequest +// | ListPromptsRequest +// | ListResourcesRequest +// | ListResourceTemplatesRequest +// | ReadResourceRequest +// | SubscribeRequest +// | UnsubscribeRequest +// | CallToolRequest +// | ListToolsRequest; + +# Represents a notification sent from the client to the server. +public type ClientNotification CancelledNotification | InitializedNotification; +// | ProgressNotification +// | InitializedNotification +// | RootsListChangedNotification; + +# Represents a result sent from the client to the server. +public type ClientResult EmptyResult; +// | CreateMessageResult | ListRootsResult; + +// Server messages +# Represents a response sent from the server to the client. +public type ServerRequest PingRequest; +// | CreateMessageRequest +// | ListRootsRequest; + +# Represents a notification sent from the server to the client. +public type ServerNotification CancelledNotification; +// | ProgressNotification +// | LoggingMessageNotification +// | ResourceUpdatedNotification +// | ResourceListChangedNotification +// | ToolListChangedNotification +// | PromptListChangedNotification; + +# Represents a result sent from the server to the client. +public type ServerResult InitializeResult | CallToolResult | ListToolsResult | EmptyResult; +// | CompleteResult +// | GetPromptResult +// | ListPromptsResult +// | ListResourceTemplatesResult +// | ListResourcesResult +// | ReadResourceResult +// | CallToolResult +// | ListToolsResult; diff --git a/ballerina/utils.bal b/ballerina/utils.bal new file mode 100644 index 0000000..2f59cd5 --- /dev/null +++ b/ballerina/utils.bal @@ -0,0 +1,87 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +function handleInitializeResponse(JSONRPCServerMessage|stream|() response) returns InitializeResult|error { + if response is stream { + (record {|JSONRPCServerMessage value;|}|error)? message = response.next(); + if message is () { + return error ClientInitializationError("Failed to receive an initialize response"); + } + if message is error { + return message; + } + JSONRPCServerMessage serverMessage = message.value; + if serverMessage is JSONRPCResponse { + ServerResult result = serverMessage.result; + if result is InitializeResult { + return result; + } else { + return error ClientInitializationError("Received unexpected response type for initialization"); + } + } else { + return error ClientInitializationError("Received unexpected response type for initialization"); + } + } else if response is JSONRPCServerMessage { + if response is JSONRPCResponse { + ServerResult result = response.result; + if result is InitializeResult { + return result; + } else { + return error ClientInitializationError("Received unexpected response type for initialization"); + } + } else { + return error ClientInitializationError("Received unexpected response type for initialization"); + } + } else { + return error ClientInitializationError("No response received for initialization"); + } +} + +function handleListToolResult(JSONRPCServerMessage|stream|() response) returns ListToolsResult|error { + if response is stream { + (record {|JSONRPCServerMessage value;|}|error)? message = response.next(); + if message is () { + return error ListToolError("Failed to receive a list tools response"); + } + if message is error { + return message; + } + JSONRPCServerMessage serverMessage = message.value; + if serverMessage is JSONRPCResponse { + ServerResult result = serverMessage.result; + if result is ListToolsResult { + return result; + } else { + return error ListToolError("Received unexpected response type for list tools"); + } + } else { + return error ListToolError("Received unexpected response type for list tools"); + } + } else if response is JSONRPCServerMessage { + if response is JSONRPCResponse { + ServerResult result = response.result; + if result is ListToolsResult { + return result; + } else { + return error ListToolError("Received unexpected response type for list tools"); + } + } else { + return error ListToolError("Received unexpected response type for list tools"); + } + } else { + return error ListToolError("No response received for list tools"); + } +} diff --git a/build.gradle b/build.gradle index 1e36124..465c725 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,7 @@ subprojects { /* Standard libraries */ ballerinaStdLibs "io.ballerina.stdlib:io-ballerina:${stdlibIoVersion}" + ballerinaStdLibs "io.ballerina.stdlib:http-ballerina:${stdlibHttpVersion}" } } diff --git a/gradle.properties b/gradle.properties index 04a74b3..0a83427 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,3 +10,4 @@ ballerinaGradlePluginVersion=2.3.0 # Dependencies stdlibIoVersion=1.8.0 +stdlibHttpVersion=2.14.1 From 07b77c17d2d397cbd08aab5947967f519c8cb663 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Tue, 27 May 2025 17:14:47 +0530 Subject: [PATCH 02/15] Add missing license headers --- ballerina/protocol.bal | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ballerina/protocol.bal b/ballerina/protocol.bal index f1f70dd..be21ed7 100644 --- a/ballerina/protocol.bal +++ b/ballerina/protocol.bal @@ -1,3 +1,19 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + # Additional initialization options. # # + enforceStrictCapabilities - Whether to restrict emitted requests to only those that the remote From 4c1ff914d154766a605ce2c9db25cffcddb317f4 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Mon, 2 Jun 2025 12:35:50 +0530 Subject: [PATCH 03/15] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 297 ------------------------------------ 1 file changed, 297 deletions(-) delete mode 100644 ballerina/Dependencies.toml diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml deleted file mode 100644 index 94a6d89..0000000 --- a/ballerina/Dependencies.toml +++ /dev/null @@ -1,297 +0,0 @@ -# AUTO-GENERATED FILE. DO NOT MODIFY. - -# This file is auto-generated by Ballerina for managing dependency versions. -# It should not be modified by hand. - -[ballerina] -dependencies-toml-version = "2" -distribution-version = "2201.12.3" - -[[package]] -org = "ballerina" -name = "auth" -version = "2.14.0" -dependencies = [ - {org = "ballerina", name = "crypto"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.array"}, - {org = "ballerina", name = "lang.string"}, - {org = "ballerina", name = "log"} -] - -[[package]] -org = "ballerina" -name = "cache" -version = "3.10.0" -dependencies = [ - {org = "ballerina", name = "constraint"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "task"}, - {org = "ballerina", name = "time"} -] - -[[package]] -org = "ballerina" -name = "constraint" -version = "1.7.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "crypto" -version = "2.9.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "time"} -] - -[[package]] -org = "ballerina" -name = "data.jsondata" -version = "1.1.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.object"} -] - -[[package]] -org = "ballerina" -name = "file" -version = "1.12.0" -dependencies = [ - {org = "ballerina", name = "io"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "os"}, - {org = "ballerina", name = "time"} -] - -[[package]] -org = "ballerina" -name = "http" -version = "2.14.1" -dependencies = [ - {org = "ballerina", name = "auth"}, - {org = "ballerina", name = "cache"}, - {org = "ballerina", name = "constraint"}, - {org = "ballerina", name = "crypto"}, - {org = "ballerina", name = "data.jsondata"}, - {org = "ballerina", name = "file"}, - {org = "ballerina", name = "io"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "jwt"}, - {org = "ballerina", name = "lang.array"}, - {org = "ballerina", name = "lang.decimal"}, - {org = "ballerina", name = "lang.int"}, - {org = "ballerina", name = "lang.regexp"}, - {org = "ballerina", name = "lang.runtime"}, - {org = "ballerina", name = "lang.string"}, - {org = "ballerina", name = "lang.value"}, - {org = "ballerina", name = "log"}, - {org = "ballerina", name = "mime"}, - {org = "ballerina", name = "oauth2"}, - {org = "ballerina", name = "observe"}, - {org = "ballerina", name = "time"}, - {org = "ballerina", name = "url"} -] -modules = [ - {org = "ballerina", packageName = "http", moduleName = "http"}, - {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} -] - -[[package]] -org = "ballerina" -name = "io" -version = "1.8.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.value"} -] -modules = [ - {org = "ballerina", packageName = "io", moduleName = "io"} -] - -[[package]] -org = "ballerina" -name = "jballerina.java" -version = "0.0.0" - -[[package]] -org = "ballerina" -name = "jwt" -version = "2.15.0" -dependencies = [ - {org = "ballerina", name = "cache"}, - {org = "ballerina", name = "crypto"}, - {org = "ballerina", name = "io"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.int"}, - {org = "ballerina", name = "lang.string"}, - {org = "ballerina", name = "log"}, - {org = "ballerina", name = "time"} -] - -[[package]] -org = "ballerina" -name = "lang.__internal" -version = "0.0.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.object"} -] - -[[package]] -org = "ballerina" -name = "lang.array" -version = "0.0.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.__internal"} -] - -[[package]] -org = "ballerina" -name = "lang.decimal" -version = "0.0.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "lang.int" -version = "0.0.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.__internal"}, - {org = "ballerina", name = "lang.object"} -] - -[[package]] -org = "ballerina" -name = "lang.object" -version = "0.0.0" - -[[package]] -org = "ballerina" -name = "lang.regexp" -version = "0.0.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "lang.runtime" -version = "0.0.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "lang.string" -version = "0.0.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.regexp"} -] - -[[package]] -org = "ballerina" -name = "lang.value" -version = "0.0.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "log" -version = "2.12.0" -dependencies = [ - {org = "ballerina", name = "io"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.value"}, - {org = "ballerina", name = "observe"} -] - -[[package]] -org = "ballerina" -name = "mcp" -version = "0.1.0" -dependencies = [ - {org = "ballerina", name = "http"}, - {org = "ballerina", name = "io"} -] -modules = [ - {org = "ballerina", packageName = "mcp", moduleName = "mcp"} -] - -[[package]] -org = "ballerina" -name = "mime" -version = "2.12.0" -dependencies = [ - {org = "ballerina", name = "io"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.int"}, - {org = "ballerina", name = "log"} -] - -[[package]] -org = "ballerina" -name = "oauth2" -version = "2.14.0" -dependencies = [ - {org = "ballerina", name = "cache"}, - {org = "ballerina", name = "crypto"}, - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "log"}, - {org = "ballerina", name = "time"}, - {org = "ballerina", name = "url"} -] - -[[package]] -org = "ballerina" -name = "observe" -version = "1.5.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "os" -version = "1.10.0" -dependencies = [ - {org = "ballerina", name = "io"}, - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "task" -version = "2.7.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "time"} -] - -[[package]] -org = "ballerina" -name = "time" -version = "2.7.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - -[[package]] -org = "ballerina" -name = "url" -version = "2.6.0" -dependencies = [ - {org = "ballerina", name = "jballerina.java"} -] - From e0399f38a10b4709c86ec8579caaf3c160e0dc35 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Tue, 3 Jun 2025 14:18:36 +0530 Subject: [PATCH 04/15] Refactor mcp client --- ballerina/Ballerina.toml | 6 + ballerina/Dependencies.toml | 313 ++++++++++++++++++ ballerina/build.gradle | 4 +- ballerina/client.bal | 161 +++++---- ballerina/error.bal | 26 +- ballerina/main.bal | 91 +++++ ...m_gen.bal => message_event_stream_gen.bal} | 29 +- ballerina/streamable_http.bal | 103 +++--- ballerina/types.bal | 188 ++++++----- ballerina/utils.bal | 58 ++-- build-config/checkstyle/build.gradle | 48 +++ build-config/resources/Ballerina.toml | 6 + build.gradle | 26 +- codecov.yml | 11 + gradle.properties | 39 ++- issue_template.md | 18 + native/build.gradle | 87 +++++ .../stdlib/mcp/MessageEventStream.java | 35 ++ native/src/main/java/module-info.java | 23 ++ pull_request_template.md | 52 +++ settings.gradle | 7 +- spotbugs-exclude.xml | 19 ++ 22 files changed, 1076 insertions(+), 274 deletions(-) create mode 100644 ballerina/Dependencies.toml create mode 100644 ballerina/main.bal rename ballerina/{server_msg_event_stream_gen.bal => message_event_stream_gen.bal} (54%) create mode 100644 build-config/checkstyle/build.gradle create mode 100644 codecov.yml create mode 100644 issue_template.md create mode 100644 native/build.gradle create mode 100644 native/src/main/java/io/ballerina/stdlib/mcp/MessageEventStream.java create mode 100644 native/src/main/java/module-info.java create mode 100644 pull_request_template.md create mode 100644 spotbugs-exclude.xml diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 7e3bc9a..c9875e2 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -11,3 +11,9 @@ distribution = "2201.12.0" [platform.java21] graalvmCompatible = true + +[[platform.java21.dependency]] +groupId = "io.ballerina.stdlib." +artifactId = "mcp-native" +version = "0.1.0" +path = "../native/build/libs/mcp-native-0.1.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml new file mode 100644 index 0000000..9792c6c --- /dev/null +++ b/ballerina/Dependencies.toml @@ -0,0 +1,313 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.12.3" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.10.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "data.jsondata" +version = "1.1.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.14.1" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "data.jsondata"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" +modules = [ + {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.15.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerina" +name = "mcp" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "mcp", moduleName = "mcp"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "uuid" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "time"} +] + diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 39835ec..5fa35cb 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -51,7 +51,8 @@ ballerina { task updateTomlFiles { doLast { - def newConfig = ballerinaTomlFilePlaceHolder.text.replace("@toml.version@", tomlVersion) + def newConfig = ballerinaTomlFilePlaceHolder.text.replace("@project.version@", project.version) + newConfig = newConfig.replace("@toml.version@", tomlVersion) ballerinaTomlFile.text = newConfig } } @@ -91,6 +92,7 @@ publishing { updateTomlFiles.dependsOn copyStdlibs build.dependsOn "generatePomFileForMavenPublication" +build.dependsOn ':mcp-native:build' publishToMavenLocal.dependsOn build publish.dependsOn build diff --git a/ballerina/client.bal b/ballerina/client.bal index ff7434b..8526efa 100644 --- a/ballerina/client.bal +++ b/ballerina/client.bal @@ -22,109 +22,140 @@ public type ClientOptions record {| ClientCapabilities capabilities?; |}; -type ServerMessageHandler function (JSONRPCServerMessage serverMessage) returns error?; - +# An MCP client on top of the Streamable HTTP transport. public distinct client class Client { + # The URL of the MCP server. private final string url; + # Information about the client, such as name and version. + private final Implementation clientInfo; + # The capabilities of the client. + private final ClientCapabilities capabilities; + + # The transport used for communication with the MCP server. private StreamableHttpClientTransport? transport = (); + # The server capabilities. private ServerCapabilities? serverCapabilities = (); + # The server version information. private Implementation? serverVersion = (); - private final Implementation clientInfo; - private ClientCapabilities capabilities; + # Request message ID for tracking requests. private int requestMessageId = 0; - private ServerMessageHandler? serverMessageHandler = (); - - function init(string url, Implementation clientInfo, ClientOptions? options = ()) { + # Initializes a new MCP client with the given URL and client information. + # + # + url - The URL of the MCP server. + # + clientInfo - Information about the client, such as name and version. + # + options - Optional capabilities to advertise as being supported by this client. + public isolated function init(string url, Implementation clientInfo, ClientOptions? options = ()) { self.url = url; self.clientInfo = clientInfo; self.capabilities = options?.capabilities ?: {}; } - public function connect() returns error? { - self.transport = check new StreamableHttpClientTransport(self.url); + # Initializes the transport and establishes a connection with the MCP server. + # + # + return - nil on success, or an `mcp:Error` on failure. + isolated remote function initialize() returns error? { + lock { + self.transport = check new StreamableHttpClientTransport(self.url); + + StreamableHttpClientTransport? transport = self.transport; + if transport is StreamableHttpClientTransport { + string? sessionId = transport.getSessionId(); + // If sessionId is non-null, it means the server is trying to reconnect + if sessionId is string { + return; + } - StreamableHttpClientTransport? transport = self.transport; - if transport is StreamableHttpClientTransport { - string? sessionId = transport.getSessionId(); - // If sessionId is non-null, it means the server is trying to reconnect - if sessionId is string { - return; - } + // If sessionId is null, it means the server is trying to establish a new connection + InitializeRequest initRequest = { + method: "initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: self.capabilities, + clientInfo: self.clientInfo + } + }; + JsonRpcMessage|stream|() response = check self.sendRequest(initRequest); + final readonly & InitializeResult initResult = (check handleInitializeResponse(response)).cloneReadOnly(); - // If sessionId is null, it means the server is trying to establish a new connection - InitializeRequest initRequest = { - method: "initialize", - params: { - protocolVersion: LATEST_PROTOCOL_VERSION, - capabilities: self.capabilities, - clientInfo: self.clientInfo + if (!SUPPORTED_PROTOCOL_VERSIONS.some(v => v == initResult.protocolVersion)) { + return error ClientInitializationError("Server's protocol version is not supported: " + initResult.protocolVersion); } - }; - JSONRPCServerMessage|stream|() response = check self.sendRequest(initRequest); - InitializeResult initResult = check handleInitializeResponse(response); + self.serverCapabilities = initResult.capabilities; + self.serverVersion = initResult.serverInfo; - if (!SUPPORTED_PROTOCOL_VERSIONS.some(v => v == initResult.protocolVersion)) { - return error ClientInitializationError("Server's protocol version is not supported: " + initResult.protocolVersion); + // Send the initialized notification + InitializedNotification initNotification = { + method: "notifications/initialized" + }; + check self.sendNotification(initNotification); + } else { + return error UninitializedTransportError("Failed to initialize transport for MCP client"); } - self.serverCapabilities = initResult.capabilities; - self.serverVersion = initResult.serverInfo; - - // Set the notification response handler - transport.setSseHandler(self.handleSseStream); - - // Send the initialized notification - InitializedNotification initNotification = { - method: "notifications/initialized" - }; - check self.sendNotification(initNotification); - } else { - return error TransportInitializationError("Failed to initialize transport for MCP client"); } } - public function listTools() returns ListToolsResult|error { + # Initializes an SSE stream, allowing the server to communicate with the client asynchronously. + # + # + return - A stream of JSON-RPC messages from the server, or an `mcp:Error` on failure. + isolated remote function subscribeToServerMessages() returns stream|error { + StreamableHttpClientTransport? transport = self.transport; + if transport is () { + return error UninitializedTransportError("Transport is not initialized for sending requests"); + } + + return check transport.startSse(); + } + + # Lists the tools available on the MCP server. + # + # + return - A list of tools available on the server, or an `mcp:Error` on failure. + isolated remote function listTools() returns ListToolsResult|error { ListToolsRequest listToolRequest = { method: "tools/list" }; - JSONRPCServerMessage|stream|() response = check self.sendRequest(listToolRequest); + JsonRpcMessage|stream|() response = check self.sendRequest(listToolRequest); ListToolsResult listToolResult = check handleListToolResult(response); return listToolResult; } - public function callTool(CallToolParams params) returns JSONRPCServerMessage|stream|error { + # Calls a tool on the MCP server with the specified parameters. + # + # + params - The parameters for the tool call, including the tool name and arguments. + # + return - The result of the tool call, or an `mcp:Error` on failure. + isolated remote function callTool(CallToolParams params) returns JsonRpcMessage|stream|error { CallToolRequest callToolRequest = { method: "tools/call", params: params }; - JSONRPCServerMessage|stream|() response = check self.sendRequest(callToolRequest); + JsonRpcMessage|stream|() response = check self.sendRequest(callToolRequest); if response is () { - return error CallToolError("Received invalid response for tool call"); + return error UninitializedTransportError("Failed to initialize transport for MCP client"); + // return error CallToolError("Received invalid response for tool call"); } return response; } - public function waitForCompletion() returns error? { + # Closes the MCP client and terminates the transport session. + # + # + return - nil on success, or an `mcp:Error` on failure. + isolated remote function close() returns error? { StreamableHttpClientTransport? transport = self.transport; if transport is () { return error UninitializedTransportError("Transport is not initialized for sending requests"); } - return transport.waitForCompletion(); + return transport.terminateSession(); } - public function setNotificationHandler(function (JSONRPCServerMessage) returns error? serverMessageHandler) returns error? { - self.serverMessageHandler = serverMessageHandler; - } - - private function sendRequest(Request request) returns JSONRPCServerMessage|stream|error? { + private isolated function sendRequest(Request request) returns JsonRpcMessage|stream|error? { StreamableHttpClientTransport? transport = self.transport; if transport is () { - return error UninitializedTransportError("Transport is not initialized for sending requests"); + return error UninitializedTransportError("Streamable HTTP transport is not initialized for sending requests"); } self.requestMessageId += 1; - JSONRPCRequest jsonrpcRequest = { + JsonRpcRequest jsonrpcRequest = { ...request, jsonrpc: JSONRPC_VERSION, id: self.requestMessageId @@ -133,31 +164,17 @@ public distinct client class Client { return transport.send(jsonrpcRequest); } - private function sendNotification(Notification notification) returns error? { + private isolated function sendNotification(Notification notification) returns error? { StreamableHttpClientTransport? transport = self.transport; if transport is () { - return error UninitializedTransportError("Transport is not initialized for sending requests"); + return error UninitializedTransportError("Streamable HTTP transport is not initialized for sending requests"); } - JSONRPCNotification jsonrpcNotification = { + JsonRpcNotification jsonrpcNotification = { ...notification, jsonrpc: JSONRPC_VERSION }; _ = check transport.send(jsonrpcNotification); - - } - - private function handleSseStream(stream 'stream) returns future { - worker SseWorker returns error? { - check from JSONRPCServerMessage serverMessage in 'stream - do { - ServerMessageHandler? handler = self.serverMessageHandler; - if handler is ServerMessageHandler { - check handler(serverMessage); - } - }; - } - return SseWorker; } } diff --git a/ballerina/error.bal b/ballerina/error.bal index 2b0ba1d..e038786 100644 --- a/ballerina/error.bal +++ b/ballerina/error.bal @@ -17,26 +17,26 @@ # Defines the common error type for the module. public type Error distinct error; -# Errors due to unsupported content type. -public type UnsupportedContentTypeError distinct Error; +# Errors related to streaming operations. +public type StreamError distinct Error; -# Errors related to transport issues. -public type TransportError distinct Error; +# Errors related to processing JSON-RPC message streams. +public type JsonRpcMessageStreamError distinct StreamError; # Errors related to client. public type ClientError distinct Error; -# Errors related to uninitialized transport. -public type UninitializedTransportError distinct TransportError; - -# Errors related to initialization of the transport. -public type TransportInitializationError distinct TransportError; +# Errors related to transport operations. +public type TransportError distinct Error; # Errors related to initialization of the client. public type ClientInitializationError distinct ClientError; -# Errors related to list tools operation. -public type ListToolError distinct ClientError; +# Errors related to uninitialized transport. +public type UninitializedTransportError distinct TransportError; -# Errors related to tool call operation. -public type CallToolError distinct ClientError; +# Errors related to streamable HTTP transport operations. +public type StreamableHttpTransportError distinct TransportError; + +# Errors due to unsupported content type. +public type UnsupportedContentTypeError distinct StreamableHttpTransportError; diff --git a/ballerina/main.bal b/ballerina/main.bal new file mode 100644 index 0000000..cfae47f --- /dev/null +++ b/ballerina/main.bal @@ -0,0 +1,91 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; + +Client mcpClient = new ("http://localhost:3000/mcp", {"name": "MCP Client", "version": "1.0.0"}); +record { + string name; + string description?; +}[] tools = []; + +// Change to remove functions in the client +public function main() returns error? { + check mcpClient->initialize(); + // stream serverMessages = check mcpClient.subscribeToServerMessages(); + // check from JSONRPCServerMessage serverMessage in serverMessages + // do { + // io:println("Received server message: ", serverMessage); + // }; + check listTools(); + // foreach var tool in tools { + // check callTool(tool.name); + // } + // // let's introduce close and let the user implement this. + // check mcpClient.waitForCompletion(); +} + +function listTools() returns error? { + ListToolsResult toolsResult = check mcpClient->listTools(); + foreach Tool tool in toolsResult.tools { + io:println("Tool Name: ", tool.name); + tools.push({ + name: tool.name, + description: tool.description + }); + } +} + +// function callTool(string name) returns error? { +// io:println("\nCalling tool: " + name); +// JSONRPCServerMessage|stream result = check mcpClient.callTool({ +// name: name, +// arguments: {"name": "itsuki"} +// }); +// if result is JSONRPCServerMessage { +// if result is JSONRPCResponse { +// ServerResult serverResult = result.result; +// if serverResult is CallToolResult { +// io:println("Tool call result: ", serverResult); +// } else { +// return error CallToolError("Received unexpected response type for tool call: " + serverResult.toString()); +// } +// } else if result is JSONRPCNotification { +// io:println("Received notification for tool call: ", result); +// } +// } else if result is stream { +// check from JSONRPCServerMessage serverMessage in 'result +// do { +// check handleMessage(serverMessage); +// }; +// } +// } + +// function executeWithDefaultValues(JSONRPCServerMessage notif) returns error? { +// io:println("Received notification: ", notif); +// } + +// function handleMessage(JSONRPCServerMessage serverMessage) returns error? { +// if serverMessage is JSONRPCResponse { +// io:println("Received message for tool call: ", serverMessage); +// } else if serverMessage is JSONRPCNotification { +// io:println("Received notification for tool call: ", serverMessage); +// } else if serverMessage is JSONRPCError { +// io:println("Received error for tool call: ", serverMessage); +// } else { +// return error CallToolError("Received unexpected response type for tool call: " + serverMessage.toString()); +// } +// } diff --git a/ballerina/server_msg_event_stream_gen.bal b/ballerina/message_event_stream_gen.bal similarity index 54% rename from ballerina/server_msg_event_stream_gen.bal rename to ballerina/message_event_stream_gen.bal index d3326ff..6a5a16b 100644 --- a/ballerina/server_msg_event_stream_gen.bal +++ b/ballerina/message_event_stream_gen.bal @@ -15,16 +15,25 @@ // under the License. import ballerina/http; +import ballerina/jballerina.java; -class ServerMessageEventStreamGenerator { - private stream eventStream; - - isolated function init(stream eventStream) returns error? { - self.eventStream = eventStream; +isolated class MessageEventStreamGenerator { + + public isolated function init(stream eventStream) { + self.externInit(eventStream); } - public isolated function next() returns record {|JSONRPCServerMessage value;|}|error? { - record {|http:SseEvent value;|}? recordVal = check self.eventStream.next(); + private isolated function externInit(stream eventStream) = @java:Method { + 'class: "io.ballerina.stdlib.mcp.MessageEventStream", + name: "initialize" + } external; + + isolated function getNextData() returns record {|http:SseEvent value;|}?|error? = @java:Method { + 'class: "io.ballerina.stdlib.mcp.MessageEventStream" + } external; + + public isolated function next() returns record {|JsonRpcMessage value;|}|error? { + record {|http:SseEvent value;|}? recordVal = check self.getNextData(); // If End of Stream if recordVal is () { @@ -33,17 +42,17 @@ class ServerMessageEventStreamGenerator { string? data = recordVal.value.data; if data is () { - return error("Received SSE event without 'data' field in the event stream."); + return error JsonRpcMessageStreamError("Received SSE event without 'data' field in the event stream."); } json jsonData = check data.fromJsonString(); - JSONRPCServerMessage rpcResponse = check jsonData.cloneWithType(); + JsonRpcMessage rpcResponse = check jsonData.cloneWithType(); return { value: rpcResponse }; }; public isolated function close() returns error? { - check self.eventStream.close(); + // check self.eventStream.close(); } } diff --git a/ballerina/streamable_http.bal b/ballerina/streamable_http.bal index b85a7c3..0e6163f 100644 --- a/ballerina/streamable_http.bal +++ b/ballerina/streamable_http.bal @@ -16,31 +16,22 @@ import ballerina/http; -# Configuration options for the StreamableHTTPClientTransport. -# -# + sessionId - Session ID for the connection. This is used to identify the session on the server. -# When not provided and connecting to a server that supports session IDs, the server will generate a new session ID. -public type StreamableHttpClientTransportOptions record {| +type StreamableHttpClientTransportOptions record {| string? sessionId = (); |}; -type SseHandler function (stream 'stream) returns future; - -distinct class StreamableHttpClientTransport { +isolated class StreamableHttpClientTransport { private final string url; private final http:Client httpClient; private string? sessionId; - private SseHandler? streamHandler = (); - future? responseFuture = (); - - function init(string url, StreamableHttpClientTransportOptions? options = ()) returns error? { + isolated function init(string url, StreamableHttpClientTransportOptions? options = ()) returns error? { self.url = url; self.httpClient = check new (url); self.sessionId = options?.sessionId; } - function send(JSONRPCMessage message) returns JSONRPCServerMessage|stream|error? { + isolated function send(JsonRpcMessage message) returns JsonRpcMessage|stream|error? { map headers = self.commonHeaders(); headers[CONTENT_TYPE_HEADER] = CONTENT_TYPE_JSON; headers[ACCEPT_HEADER] = string `${CONTENT_TYPE_JSON}, ${CONTENT_TYPE_SSE}`; @@ -49,21 +40,14 @@ distinct class StreamableHttpClientTransport { // Handle sessionId during the initialization request string|error sessionId = response.getHeader(SESSION_ID_HEADER); - if sessionId is string { - self.sessionId = sessionId; + lock { + if sessionId is string { + self.sessionId = sessionId; + } } - // TODO: Handle Authorization - // If the response is 202 Accepted, there's no body to process if response.statusCode == 202 { - if (self.isInitializedNotification(message)) { - stream serverMsgEventStream = check self.startSse(); - SseHandler? streamHandler = self.streamHandler; - if streamHandler is SseHandler { - self.responseFuture = streamHandler(serverMsgEventStream); - } - } return; } @@ -71,59 +55,62 @@ distinct class StreamableHttpClientTransport { string contentType = response.getContentType(); if contentType.includes(CONTENT_TYPE_SSE) { stream sseEventStream = check response.getSseEventStream(); - ServerMessageEventStreamGenerator serverMsgEventStreamGenerator = check new (sseEventStream); - stream serverMsgEventStream = new (serverMsgEventStreamGenerator); - return serverMsgEventStream; + MessageEventStreamGenerator msgEventStreamGenerator = new (sseEventStream); + stream msgEventStream = new (msgEventStreamGenerator); + return msgEventStream; } else if contentType.includes(CONTENT_TYPE_JSON) { json jsonPayload = check response.getJsonPayload(); - JSONRPCServerMessage serverMsg = check jsonPayload.cloneWithType(); + JsonRpcMessage serverMsg = check jsonPayload.cloneWithType(); return serverMsg; } else { return error UnsupportedContentTypeError("Unsupported content type: " + contentType); } } - function getSessionId() returns string? { - return self.sessionId; - } + isolated function startSse() returns stream|error { + map headers = self.commonHeaders(); + headers[ACCEPT_HEADER] = "text/event-stream"; - function setSseHandler(SseHandler handler) { - self.streamHandler = handler; + stream sseEventStream = check self.httpClient->get("/", headers = headers); + MessageEventStreamGenerator msgEventStreamGenerator = new (sseEventStream); + stream msgEventStream = new (msgEventStreamGenerator); + return msgEventStream; } - function waitForCompletion() returns error? { - future? responseFuture = self.responseFuture; - if (responseFuture is ()) { - return (); - } - return wait responseFuture; - } + isolated function terminateSession() returns error? { + lock { + if (self.sessionId is ()) { + return; + } - private function startSse() returns stream|error { - map headers = self.commonHeaders(); - headers[ACCEPT_HEADER] = "text/event-stream"; + map headers = self.commonHeaders(); + headers[CONTENT_TYPE_HEADER] = CONTENT_TYPE_JSON; - stream sseEventStream = check self.httpClient->get("/", headers = headers); - ServerMessageEventStreamGenerator serverMsgEventStreamGenerator = check new (sseEventStream); - stream serverMsgEventStream = new (serverMsgEventStreamGenerator); - return serverMsgEventStream; + http:Response response = check self.httpClient->delete("/", headers = headers); + + if response.statusCode == 405 { + return error TransportError("Session termination not supported by the server"); + } + + self.sessionId = (); + return; + } } - private function commonHeaders() returns map { - map headers = {}; - string? sessionId = self.sessionId; - if (sessionId is string) { - headers[SESSION_ID_HEADER] = sessionId; + isolated function getSessionId() returns string? { + lock { + return self.sessionId; } - return headers; } - private function isInitializedNotification(JSONRPCServerMessage message) returns boolean { - if message is JSONRPCNotification { - if message.method == "notifications/initialized" { - return true; + private isolated function commonHeaders() returns map { + lock { + map headers = {}; + string? sessionId = self.sessionId; + if (sessionId is string) { + headers[SESSION_ID_HEADER] = sessionId; } + return headers.clone(); } - return false; } } diff --git a/ballerina/types.bal b/ballerina/types.bal index d5931e2..b9a98cf 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -15,10 +15,7 @@ // under the License. # Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. -public type JSONRPCMessage JSONRPCRequest|JSONRPCNotification|JSONRPCResponse|JSONRPCError; - -# Refers to any valid JSON-RPC object that can be decoded off the wire. -public type JSONRPCServerMessage JSONRPCNotification|JSONRPCResponse|JSONRPCError; +public type JsonRpcMessage JsonRpcRequest|JsonRpcNotification|JsonRpcResponse|JsonRpcError; public const LATEST_PROTOCOL_VERSION = "2025-03-26"; public const SUPPORTED_PROTOCOL_VERSIONS = [ @@ -76,7 +73,7 @@ public type RequestId string|int; # # + jsonrpc - The JSON-RPC protocol version # + id - Identifier established by the client that should be returned in the response -public type JSONRPCRequest record { +public type JsonRpcRequest record { *Request; JSONRPC_VERSION jsonrpc; RequestId id; @@ -85,7 +82,7 @@ public type JSONRPCRequest record { # A notification which does not expect a response. # # + jsonrpc - The JSON-RPC protocol version -public type JSONRPCNotification record { +public type JsonRpcNotification record { *Notification; JSONRPC_VERSION jsonrpc; }; @@ -95,7 +92,7 @@ public type JSONRPCNotification record { # + jsonrpc - The JSON-RPC protocol version # + id - Identifier of the request # + result - The result of the request -public type JSONRPCResponse record {| +public type JsonRpcResponse record {| JSONRPC_VERSION jsonrpc; RequestId id; ServerResult result; @@ -113,7 +110,7 @@ public const INTERNAL_ERROR = -32603; # + jsonrpc - The JSON-RPC protocol version # + id - Identifier of the request # + error - The error information -public type JSONRPCError record { +public type JsonRpcError record { JSONRPC_VERSION jsonrpc; RequestId id; record { @@ -260,7 +257,29 @@ public type PingRequest record {| *Request; "ping" method; |}; -// ProgressNotification + +# An out-of-band notification used to inform the receiver of a progress update for a long-running request. +# +# + method - The method name for the notification +# + params - The parameters for the progress notification +public type ProgressNotification record {| + *Notification; + "notifications/progress" method; + record { + # The progress token which was given in the initial request, + # used to associate this notification with the request that is proceeding. + ProgressToken progressToken; + # The progress thus far. This should increase every time progress is made, + # even if the total is unknown. + int progress; + # Total number of items to process (or total progress required), if known. + int total?; + # An optional message describing the current progress. + string message?; + record {} _meta?; + } params; +|}; + # Represents a paginated request with optional cursor-based pagination. # # + params - Optional pagination parameters @@ -280,18 +299,32 @@ public type PaginatedResult record {| *Result; Cursor nextCursor?; |}; -// ListResourcesRequest -// ListResourcesResult -// ListResourceTemplatesRequest -// ListResourceTemplatesResult -// ReadResourceRequest -// ReadResourceResult -// ResourceListChangedNotification -// SubscribeRequest -// UnsubscribeRequest -// ResourceUpdatedNotification -// Resource -// ResourceTemplate + +# An optional notification from the server to the client, informing it that the list of resources it can read from has changed. +# This may be issued by servers without any previous subscription from the client. +# +# + method - The JSON-RPC method name for resource list changed notifications +public type ResourceListChangedNotification record {| + *Notification; + "notifications/resources/list_changed" method; +|}; + +# A notification from the server to the client, informing it that a resource has changed and may need to be read again. +# This should only be sent if the client previously sent a resources/subscribe request. +# +# + method - The JSON-RPC method name for resource updated notifications +public type ResourceUpdatedNotification record {| + *Notification; + "notifications/resources/updated" method; + # The parameters for the resource updated notification + record { + # The URI of the resource that has been updated. This might be a sub-resource of the one + # that the client actually subscribed to. + string uri; + record {} _meta?; + } params; +|}; + # The contents of a specific resource or sub-resource. # # + uri - The URI of this resource. @@ -317,15 +350,9 @@ public type BlobResourceContents record {| string blob; |}; -// ListPromptsRequest -// ListPromptsResult -// GetPromptRequest -// GetPromptResult -// Prompt -// PromptArgument # The sender or recipient of messages and data in a conversation. public type Role "user"|"assistant"; -// PromptMessage + # The contents of a resource, embedded into a prompt or tool call result. # # It is up to the client how best to render embedded resources for the benefit @@ -339,7 +366,17 @@ public type EmbeddedResource record {| TextResourceContents|BlobResourceContents 'resource; Annotations annotations?; |}; -// PromptListChangedNotification + +# An optional notification from the server to the client, informing it that +# the list of prompts it offers has changed. This may be issued by servers +# without any previous subscription from the client. +# +# + method - The JSON-RPC method name for prompt list changed notifications +public type PromptListChangedNotification record {| + *Notification; + "notifications/prompts/list_changed" method; +|}; + # Sent from the client to request a list of tools the server has. # # + method - The method identifier for this request @@ -392,7 +429,16 @@ public type CallToolParams record {| string name; record {} arguments?; |}; -// ToolListChangedNotification + +# An optional notification from the server to the client, informing it that the list of tools +# it offers has changed. This may be issued by servers without any previous subscription from the client. +# +# + method - The JSON-RPC method name for tool list changed notifications +public type ToolListChangedNotification record {| + *Notification; + "notifications/tools/list_changed" method; +|}; + # Additional properties describing a Tool to clients. # NOTE: all properties in ToolAnnotations are **hints**. # They are not guaranteed to provide a faithful description of @@ -441,11 +487,32 @@ public type Tool record {| } inputSchema; ToolAnnotations annotations?; |}; -// SetLevelRequest -// LoggingMessageNotification -// CreateMessageRequest -// CreateMessageResult -// SamplingMessage + +# Notification of a log message passed from server to client. If no logging/setLevel request has been +# sent from the client, the server MAY decide which messages to send automatically. +# +# + method - The method name for the notification +# + params - The parameters for the logging message notification +public type LoggingMessageNotification record {| + *Notification; + "notifications/message" method; + record { + # The severity of this log message. + LoggingLevel level; + # An optional name of the logger issuing this message. + string logger?; + # The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + anydata data; + record {} _meta?; + } params; +|}; + +# The severity of a log message. +# +# These map to syslog message severities, as specified in RFC-5424: +# https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 +public type LoggingLevel "debug"|"info"|"notice"|"warning"|"error"|"critical"|"alert"|"emergency"; + # Optional annotations for the client. The client can use annotations to inform how objects are used or displayed # # + audience - Describes who the intended customer of this object or data is. @@ -494,64 +561,29 @@ public type AudioContent record {| string mimeType; Annotations annotations?; |}; -// ModelPreferences -// ModelHint -// CompleteRequest -// CompleteResult -// ResourceReference -// PromptReference -// ListRootsRequest -// ListRootsResult -// Root -// RootsListChangedNotification // Client messages # Represents a request sent from the client to the server. public type ClientRequest PingRequest | InitializeRequest | CallToolRequest | ListToolsRequest; -// | CompleteRequest -// | SetLevelRequest -// | GetPromptRequest -// | ListPromptsRequest -// | ListResourcesRequest -// | ListResourceTemplatesRequest -// | ReadResourceRequest -// | SubscribeRequest -// | UnsubscribeRequest -// | CallToolRequest -// | ListToolsRequest; # Represents a notification sent from the client to the server. -public type ClientNotification CancelledNotification | InitializedNotification; -// | ProgressNotification -// | InitializedNotification -// | RootsListChangedNotification; +public type ClientNotification CancelledNotification | ProgressNotification | InitializedNotification; # Represents a result sent from the client to the server. public type ClientResult EmptyResult; -// | CreateMessageResult | ListRootsResult; // Server messages # Represents a response sent from the server to the client. public type ServerRequest PingRequest; -// | CreateMessageRequest -// | ListRootsRequest; # Represents a notification sent from the server to the client. -public type ServerNotification CancelledNotification; -// | ProgressNotification -// | LoggingMessageNotification -// | ResourceUpdatedNotification -// | ResourceListChangedNotification -// | ToolListChangedNotification -// | PromptListChangedNotification; +public type ServerNotification CancelledNotification + | ProgressNotification + | LoggingMessageNotification + | ResourceUpdatedNotification + | ResourceListChangedNotification + | ToolListChangedNotification + | PromptListChangedNotification; # Represents a result sent from the server to the client. public type ServerResult InitializeResult | CallToolResult | ListToolsResult | EmptyResult; -// | CompleteResult -// | GetPromptResult -// | ListPromptsResult -// | ListResourceTemplatesResult -// | ListResourcesResult -// | ReadResourceResult -// | CallToolResult -// | ListToolsResult; diff --git a/ballerina/utils.bal b/ballerina/utils.bal index 2f59cd5..8d9fe96 100644 --- a/ballerina/utils.bal +++ b/ballerina/utils.bal @@ -14,74 +14,60 @@ // specific language governing permissions and limitations // under the License. -function handleInitializeResponse(JSONRPCServerMessage|stream|() response) returns InitializeResult|error { - if response is stream { - (record {|JSONRPCServerMessage value;|}|error)? message = response.next(); +isolated function handleInitializeResponse(JsonRpcMessage|stream|() response) returns InitializeResult|error { + if response is stream { + (record {|JsonRpcMessage value;|}|error)? message = response.next(); if message is () { - return error ClientInitializationError("Failed to receive an initialize response"); + // return error ClientInitializationError("Failed to receive an initialize response"); + return error("Error"); } if message is error { return message; } - JSONRPCServerMessage serverMessage = message.value; - if serverMessage is JSONRPCResponse { + JsonRpcMessage serverMessage = message.value; + if serverMessage is JsonRpcResponse { ServerResult result = serverMessage.result; if result is InitializeResult { return result; - } else { - return error ClientInitializationError("Received unexpected response type for initialization"); } - } else { - return error ClientInitializationError("Received unexpected response type for initialization"); } - } else if response is JSONRPCServerMessage { - if response is JSONRPCResponse { + } else if response is JsonRpcMessage { + if response is JsonRpcResponse { ServerResult result = response.result; if result is InitializeResult { return result; - } else { - return error ClientInitializationError("Received unexpected response type for initialization"); } - } else { - return error ClientInitializationError("Received unexpected response type for initialization"); } - } else { - return error ClientInitializationError("No response received for initialization"); } + return error("Error"); + // return error ClientInitializationError("No response received for initialization"); } -function handleListToolResult(JSONRPCServerMessage|stream|() response) returns ListToolsResult|error { - if response is stream { - (record {|JSONRPCServerMessage value;|}|error)? message = response.next(); +isolated function handleListToolResult(JsonRpcMessage|stream|() response) returns ListToolsResult|error { + if response is stream { + (record {|JsonRpcMessage value;|}|error)? message = response.next(); if message is () { - return error ListToolError("Failed to receive a list tools response"); + // return error ListToolError("Failed to receive a list tools response"); + return error("Error"); } if message is error { return message; } - JSONRPCServerMessage serverMessage = message.value; - if serverMessage is JSONRPCResponse { + JsonRpcMessage serverMessage = message.value; + if serverMessage is JsonRpcResponse { ServerResult result = serverMessage.result; if result is ListToolsResult { return result; - } else { - return error ListToolError("Received unexpected response type for list tools"); } - } else { - return error ListToolError("Received unexpected response type for list tools"); } - } else if response is JSONRPCServerMessage { - if response is JSONRPCResponse { + } else if response is JsonRpcMessage { + if response is JsonRpcResponse { ServerResult result = response.result; if result is ListToolsResult { return result; - } else { - return error ListToolError("Received unexpected response type for list tools"); } - } else { - return error ListToolError("Received unexpected response type for list tools"); } - } else { - return error ListToolError("No response received for list tools"); } + return error("Error"); + // return error ListToolError("No response received for list tools"); } diff --git a/build-config/checkstyle/build.gradle b/build-config/checkstyle/build.gradle new file mode 100644 index 0000000..dee55ba --- /dev/null +++ b/build-config/checkstyle/build.gradle @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id "de.undercouch.download" +} + +apply plugin: 'java' + +task downloadCheckstyleRuleFiles(type: Download) { + src([ + 'https://raw.githubusercontent.com/wso2/code-quality-tools/v1.4/checkstyle/jdk-17/checkstyle.xml', + 'https://raw.githubusercontent.com/wso2/code-quality-tools/v1.4/checkstyle/jdk-17/suppressions.xml' + ]) + overwrite false + onlyIfNewer true + dest buildDir +} + +jar { + enabled = false +} + +clean { + enabled = false +} + +artifacts.add('default', file("$project.buildDir/checkstyle.xml")) { + builtBy('downloadCheckstyleRuleFiles') +} + +artifacts.add('default', file("$project.buildDir/suppressions.xml")) { + builtBy('downloadCheckstyleRuleFiles') +} diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 01d4c88..8e2fae7 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -11,3 +11,9 @@ distribution = "2201.12.0" [platform.java21] graalvmCompatible = true + +[[platform.java21.dependency]] +groupId = "io.ballerina.stdlib." +artifactId = "mcp-native" +version = "@toml.version@" +path = "../native/build/libs/mcp-native-@project.version@.jar" diff --git a/build.gradle b/build.gradle index 465c725..caecffa 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ */ plugins { + id "com.github.spotbugs-base" id "com.github.johnrengelman.shadow" id "de.undercouch.download" id "net.researchgate.release" @@ -70,8 +71,28 @@ subprojects { } /* Standard libraries */ - ballerinaStdLibs "io.ballerina.stdlib:io-ballerina:${stdlibIoVersion}" + ballerinaStdLibs "io.ballerina.stdlib:auth-ballerina:${stdlibAuthVersion}" + ballerinaStdLibs "io.ballerina.stdlib:cache-ballerina:${stdlibCacheVersion}" + ballerinaStdLibs "io.ballerina.stdlib:constraint-ballerina:${stdlibConstraintVersion}" + ballerinaStdLibs "io.ballerina.stdlib:crypto-ballerina:${stdlibCryptoVersion}" + ballerinaStdLibs "io.ballerina.stdlib:file-ballerina:${stdlibFileVersion}" ballerinaStdLibs "io.ballerina.stdlib:http-ballerina:${stdlibHttpVersion}" + ballerinaStdLibs "io.ballerina.stdlib:io-ballerina:${stdlibIoVersion}" + ballerinaStdLibs "io.ballerina.stdlib:jwt-ballerina:${stdlibJwtVersion}" + ballerinaStdLibs "io.ballerina.stdlib:log-ballerina:${stdlibLogVersion}" + ballerinaStdLibs "io.ballerina.stdlib:mime-ballerina:${stdlibMimeVersion}" + ballerinaStdLibs "io.ballerina.stdlib:oauth2-ballerina:${stdlibOAuth2Version}" + ballerinaStdLibs "io.ballerina.stdlib:os-ballerina:${stdlibOsVersion}" + ballerinaStdLibs "io.ballerina.stdlib:task-ballerina:${stdlibTaskVersion}" + ballerinaStdLibs "io.ballerina.stdlib:time-ballerina:${stdlibTimeVersion}" + ballerinaStdLibs "io.ballerina.stdlib:url-ballerina:${stdlibUrlVersion}" + ballerinaStdLibs "io.ballerina.stdlib:yaml-ballerina:${stdlibYamlVersion}" + ballerinaStdLibs "io.ballerina.stdlib:xmldata-ballerina:${stdlibXmldataVersion}" + ballerinaStdLibs "io.ballerina.stdlib:uuid-ballerina:${stdlibUuidVersion}" + + ballerinaStdLibs "io.ballerina.stdlib:observe-ballerina:${observeVersion}" + ballerinaStdLibs "io.ballerina:observe-ballerina:${observeInternalVersion}" + ballerinaStdLibs "io.ballerina.lib:data.jsondata-ballerina:${stdlibDataJsonDataVersion}" } } @@ -92,5 +113,6 @@ release { } task build { - dependsOn('mcp-ballerina:build') + dependsOn(':mcp-ballerina:build') + dependsOn(':mcp-native:build') } diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..b6e25d8 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,11 @@ +fixes: + - "io/ballerina/lib/ai/plugin/::./compiler-plugin/src/main/java/io/ballerina/lib/ai/plugin/" + +coverage: + precision: 2 + round: down + range: "60...80" + status: + project: + default: + target: 80 diff --git a/gradle.properties b/gradle.properties index 0a83427..fec109c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,11 +3,46 @@ group=io.ballerina.stdlib version=0.1.0-SNAPSHOT ballerinaLangVersion=2201.12.0 +ballerinaGradlePluginVersion=2.3.0 + +checkstylePluginVersion=10.12.0 +spotbugsPluginVersion=6.0.18 + shadowJarPluginVersion=8.1.1 downloadPluginVersion=5.4.0 releasePluginVersion=2.8.0 -ballerinaGradlePluginVersion=2.3.0 -# Dependencies + +# Ballerina Library Dependencies +# Level 01 stdlibIoVersion=1.8.0 +stdlibTimeVersion=2.7.0 +stdlibUrlVersion=2.6.0 +stdlibXmldataVersion=2.9.0 + +# Level 02 +stdlibConstraintVersion=1.7.0 +stdlibCryptoVersion=2.9.0 +stdlibLogVersion=2.12.0 +stdlibOsVersion=1.10.0 +stdlibTaskVersion=2.10.0 + +# Level 03 +stdlibCacheVersion=3.10.0 +stdlibFileVersion=1.12.0 +stdlibMimeVersion=2.12.0 +stdlibUuidVersion=1.10.0 + +# Level 04 +stdlibAuthVersion=2.14.0 +stdlibDataJsonDataVersion=1.1.0 +stdlibJwtVersion=2.15.0 +stdlibOAuth2Version=2.14.0 +stdlibYamlVersion=0.8.0 + +# Level 05 stdlibHttpVersion=2.14.1 + +# Ballerinax Observer +observeVersion=1.5.0 +observeInternalVersion=1.5.0 diff --git a/issue_template.md b/issue_template.md new file mode 100644 index 0000000..52a7563 --- /dev/null +++ b/issue_template.md @@ -0,0 +1,18 @@ +**Description:** + + +**Suggested Labels:** + + +**Suggested Assignees:** + + +**Affected Product Version:** + +**OS, DB, other environment details and versions:** + +**Steps to reproduce:** + + +**Related Issues:** + diff --git a/native/build.gradle b/native/build.gradle new file mode 100644 index 0000000..eb2eeb2 --- /dev/null +++ b/native/build.gradle @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.github.spotbugs' + id 'checkstyle' +} + +description = 'Ballerina - MCP package Java Utils' + +dependencies { + implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-runtime', version: "${ballerinaLangVersion}" + checkstyle project(":checkstyle") + checkstyle "com.puppycrawl.tools:checkstyle:${checkstylePluginVersion}" +} + +spotbugsMain { + def classLoader = plugins["com.github.spotbugs"].class.classLoader + def SpotBugsConfidence = classLoader.findLoadedClass("com.github.spotbugs.snom.Confidence") + def SpotBugsEffort = classLoader.findLoadedClass("com.github.spotbugs.snom.Effort") + ignoreFailures = true + effort = SpotBugsEffort.MAX + reportLevel = SpotBugsConfidence.LOW + reportsDir = file("$project.buildDir/reports/spotbugs") + def excludeFile = file("${rootDir}/build-config/spotbugs-exclude.xml") + if (excludeFile.exists()) { + it.excludeFilter = excludeFile + } + reports { + text.enabled = true + } +} + +spotbugsTest { + enabled = false +} + +task validateSpotbugs() { + doLast { + if (spotbugsMain.reports.size() > 0 && + spotbugsMain.reports[0].destination.exists() && + spotbugsMain.reports[0].destination.text.readLines().size() > 0) { + spotbugsMain.reports[0].destination?.eachLine { + println 'Failure: ' + it + } + throw new GradleException("Spotbugs rule violations were found."); + } + } +} + +checkstyle { + toolVersion '7.8.2' + configFile file("${rootDir}/build-config/checkstyle/build/checkstyle.xml") + configProperties = ["suppressionFile": file("${rootDir}/build-config/checkstyle/build/suppressions.xml")] +} + +tasks.withType(Checkstyle) { + exclude '**/module-info.java' +} + +spotbugsMain.finalizedBy validateSpotbugs +checkstyleMain.dependsOn(":checkstyle:downloadCheckstyleRuleFiles") + +compileJava { + doFirst { + options.compilerArgs = [ + '--module-path', classpath.asPath, + ] + classpath = files() + } +} \ No newline at end of file diff --git a/native/src/main/java/io/ballerina/stdlib/mcp/MessageEventStream.java b/native/src/main/java/io/ballerina/stdlib/mcp/MessageEventStream.java new file mode 100644 index 0000000..9ef454f --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/mcp/MessageEventStream.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.mcp; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BStream; + +public class MessageEventStream { + public static void initialize(BObject object, BStream eventStream) { + object.addNativeData("stream", eventStream); + } + + public static Object getNextData(Environment env, BObject object) { + BStream eventStream = (BStream) object.getNativeData("stream"); + BObject iteratorObj = eventStream.getIteratorObj(); + return env.getRuntime().callMethod(iteratorObj, "next", null); + } +} diff --git a/native/src/main/java/module-info.java b/native/src/main/java/module-info.java new file mode 100644 index 0000000..5f904c8 --- /dev/null +++ b/native/src/main/java/module-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module io.ballerina.stdlib.mcp { + requires io.ballerina.runtime; + requires io.ballerina.lang; + exports io.ballerina.stdlib.mcp; +} diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..45221b5 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,52 @@ +## Purpose +> Describe the problems, issues, or needs driving this feature/fix and include links to related issues in the following format: Resolves issue1, issue2, etc. + +## Goals +> Describe the solutions that this feature/fix will introduce to resolve the problems described above + +## Approach +> Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI (email documentation@wso2.com to review all UI text). Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here. + +## User stories +> Summary of user stories addressed by this change> + +## Release note +> Brief description of the new feature or bug fix as it will appear in the release notes + +## Documentation +> Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter “N/A” plus brief explanation of why there’s no doc impact + +## Training +> Link to the PR for changes to the training content in https://github.com/wso2/WSO2-Training, if applicable + +## Certification +> Type “Sent” when you have provided new/updated certification questions, plus four answers for each question (correct answer highlighted in bold), based on this change. Certification questions/answers should be sent to certification@wso2.com and NOT pasted in this PR. If there is no impact on certification exams, type “N/A” and explain why. + +## Marketing +> Link to drafts of marketing content that will describe and promote this feature, including product page changes, technical articles, blog posts, videos, etc., if applicable + +## Automation tests + - Unit tests + > Code coverage information + - Integration tests + > Details about the test cases and coverage + +## Security checks + - Followed secure coding standards in http://wso2.com/technical-reports/wso2-secure-engineering-guidelines? yes/no + - Ran FindSecurityBugs plugin and verified report? yes/no + - Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets? yes/no + +## Samples +> Provide high-level details about the samples related to this feature + +## Related PRs +> List any other related PRs + +## Migrations (if applicable) +> Describe migration steps and platforms on which migration has been tested + +## Test environment +> List all JDK versions, operating systems, databases, and browser/versions on which this feature/fix was tested + +## Learning +> Describe the research phase and any blog posts, patterns, libraries, or add-ons you used to solve the problem. diff --git a/settings.gradle b/settings.gradle index 1303d56..a00d7fb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,7 @@ pluginManagement { plugins { + id "com.github.spotbugs-base" version "${spotbugsPluginVersion}" id "com.github.johnrengelman.shadow" version "${shadowJarPluginVersion}" id "de.undercouch.download" version "${downloadPluginVersion}" id "net.researchgate.release" version "${releasePluginVersion}" @@ -33,9 +34,13 @@ plugins { rootProject.name = 'mcp' +include ':checkstyle' include ':mcp-ballerina' +include ':mcp-native' +project(':checkstyle').projectDir = file("build-config${File.separator}checkstyle") project(':mcp-ballerina').projectDir = file('ballerina') +project(':mcp-native').projectDir = file('native') gradleEnterprise { buildScan { @@ -43,5 +48,3 @@ gradleEnterprise { termsOfServiceAgree = 'yes' } } - - diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml new file mode 100644 index 0000000..58da414 --- /dev/null +++ b/spotbugs-exclude.xml @@ -0,0 +1,19 @@ + + + From ba8d51e4deac7264127b8aeca87cc932bb47bcc7 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Tue, 3 Jun 2025 14:19:51 +0530 Subject: [PATCH 05/15] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 9792c6c..e1c1367 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.12.3" +distribution-version = "2201.12.0" [[package]] org = "ballerina" From 69f7d21c1095ab8f3b619f838b22a0d6c4c634f8 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Tue, 3 Jun 2025 14:22:45 +0530 Subject: [PATCH 06/15] Fix build failure --- ballerina/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 5fa35cb..7c8a749 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -92,7 +92,8 @@ publishing { updateTomlFiles.dependsOn copyStdlibs build.dependsOn "generatePomFileForMavenPublication" -build.dependsOn ':mcp-native:build' +build.dependsOn ":${packageName}-native:build" +test.dependsOn ":${packageName}-native:build" publishToMavenLocal.dependsOn build publish.dependsOn build From d381be72e3e62fe3fbc86ec828b93fdd0d665574 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Tue, 3 Jun 2025 15:06:57 +0530 Subject: [PATCH 07/15] Refactor mcp client apis --- ballerina/client.bal | 265 +++++++++++------- ballerina/error.bal | 72 ++++- ballerina/init.bal | 25 ++ .../json_rpc_message_stream_transformer.bal | 112 ++++++++ ballerina/main.bal | 91 ------ ballerina/message_event_stream_gen.bal | 58 ---- ballerina/protocol.bal | 12 +- ballerina/streamable_http.bal | 181 ++++++++---- ballerina/types.bal | 66 ++--- ballerina/utils.bal | 108 +++---- ...ssageEventStream.java => ModuleUtils.java} | 22 +- .../stdlib/mcp/SseEventStreamHelper.java | 93 ++++++ 12 files changed, 678 insertions(+), 427 deletions(-) create mode 100644 ballerina/init.bal create mode 100644 ballerina/json_rpc_message_stream_transformer.bal delete mode 100644 ballerina/main.bal delete mode 100644 ballerina/message_event_stream_gen.bal rename native/src/main/java/io/ballerina/stdlib/mcp/{MessageEventStream.java => ModuleUtils.java} (58%) create mode 100644 native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java diff --git a/ballerina/client.bal b/ballerina/client.bal index 8526efa..6b78916 100644 --- a/ballerina/client.bal +++ b/ballerina/client.bal @@ -14,167 +14,218 @@ // specific language governing permissions and limitations // under the License. -# Client options for protocol initialization. +# Configuration options for initializing an MCP client. # -# + capabilities - Capabilities to advertise as being supported by this client. -public type ClientOptions record {| +# + capabilities - Capabilities to be advertised by this client. +public type ClientConfiguration record {| *ProtocolOptions; ClientCapabilities capabilities?; |}; -# An MCP client on top of the Streamable HTTP transport. +# Represents an MCP client built on top of the Streamable HTTP transport. public distinct client class Client { - # The URL of the MCP server. - private final string url; - # Information about the client, such as name and version. + # MCP server URL. + private final string serverUrl; + # Client implementation details (e.g., name and version). private final Implementation clientInfo; - # The capabilities of the client. - private final ClientCapabilities capabilities; + # Capabilities supported by the client. + private final ClientCapabilities clientCapabilities; - # The transport used for communication with the MCP server. + # Transport for communication with the MCP server. private StreamableHttpClientTransport? transport = (); - # The server capabilities. + # Server capabilities. private ServerCapabilities? serverCapabilities = (); - # The server version information. - private Implementation? serverVersion = (); - # Request message ID for tracking requests. - private int requestMessageId = 0; - - # Initializes a new MCP client with the given URL and client information. - # - # + url - The URL of the MCP server. - # + clientInfo - Information about the client, such as name and version. - # + options - Optional capabilities to advertise as being supported by this client. - public isolated function init(string url, Implementation clientInfo, ClientOptions? options = ()) { - self.url = url; + # Server implementation information. + private Implementation? serverInfo = (); + # Request ID generator for tracking requests. + private int requestId = 0; + + # Initializes a new MCP client with the provided server URL and client details. + # + # + serverUrl - MCP server URL. + # + clientInfo - Client details, such as name and version. + # + config - Optional configuration containing client capabilities. + public isolated function init(string serverUrl, Implementation clientInfo, ClientConfiguration? config = ()) { + self.serverUrl = serverUrl; self.clientInfo = clientInfo; - self.capabilities = options?.capabilities ?: {}; + self.clientCapabilities = config?.capabilities ?: {}; } - # Initializes the transport and establishes a connection with the MCP server. - # - # + return - nil on success, or an `mcp:Error` on failure. - isolated remote function initialize() returns error? { + # Establishes a connection to the MCP server and performs protocol initialization. + # + # + return - A ClientError if initialization fails, or nil on success. + isolated remote function initialize() returns ClientError? { lock { - self.transport = check new StreamableHttpClientTransport(self.url); - - StreamableHttpClientTransport? transport = self.transport; - if transport is StreamableHttpClientTransport { - string? sessionId = transport.getSessionId(); - // If sessionId is non-null, it means the server is trying to reconnect - if sessionId is string { - return; + // Create and initialize transport. + StreamableHttpClientTransport newTransport = check new StreamableHttpClientTransport(self.serverUrl); + self.transport = newTransport; + + string? sessionId = newTransport.getSessionId(); + + // If a session ID exists, assume reconnection and skip initialization. + if sessionId is string { + return; + } + + // Prepare and send the initialization request. + InitializeRequest initRequest = { + method: "initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: self.clientCapabilities, + clientInfo: self.clientInfo } + }; - // If sessionId is null, it means the server is trying to establish a new connection - InitializeRequest initRequest = { - method: "initialize", - params: { - protocolVersion: LATEST_PROTOCOL_VERSION, - capabilities: self.capabilities, - clientInfo: self.clientInfo - } - }; - JsonRpcMessage|stream|() response = check self.sendRequest(initRequest); - final readonly & InitializeResult initResult = (check handleInitializeResponse(response)).cloneReadOnly(); + ServerResult response = check self.sendRequestMessage(initRequest); - if (!SUPPORTED_PROTOCOL_VERSIONS.some(v => v == initResult.protocolVersion)) { - return error ClientInitializationError("Server's protocol version is not supported: " + initResult.protocolVersion); + if response is InitializeResult { + final readonly & string protocolVersion = response.protocolVersion; + // Validate protocol compatibility. + if (!SUPPORTED_PROTOCOL_VERSIONS.some(v => v == protocolVersion)) { + return error ProtocolVersionError( + string `Server protocol version '${protocolVersion}' is not supported. Supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.toString()}.` + ); } - self.serverCapabilities = initResult.capabilities; - self.serverVersion = initResult.serverInfo; - // Send the initialized notification + // Store server capabilities and info. + self.serverCapabilities = response.capabilities; + self.serverInfo = response.serverInfo; + + // Send notification to complete initialization. InitializedNotification initNotification = { method: "notifications/initialized" }; - check self.sendNotification(initNotification); + check self.sendNotificationMessage(initNotification); + + return; } else { - return error UninitializedTransportError("Failed to initialize transport for MCP client"); + return error ClientInitializationError( + string `Initialization failed: unexpected response type '${(typeof response).toString()}' received from server.` + ); } } } - # Initializes an SSE stream, allowing the server to communicate with the client asynchronously. - # - # + return - A stream of JSON-RPC messages from the server, or an `mcp:Error` on failure. - isolated remote function subscribeToServerMessages() returns stream|error { - StreamableHttpClientTransport? transport = self.transport; - if transport is () { - return error UninitializedTransportError("Transport is not initialized for sending requests"); + # Opens a server-sent events (SSE) stream for asynchronous server-to-client communication. + # + # + return - Stream of JsonRpcMessages or a ClientError. + isolated remote function subscribeToServerMessages() returns stream|ClientError { + StreamableHttpClientTransport? currentTransport = self.transport; + if currentTransport is () { + return error UninitializedTransportError( + "Subscription failed: client transport is not initialized. Call initialize() first." + ); } - - return check transport.startSse(); + return check currentTransport.establishEventStream(); } - # Lists the tools available on the MCP server. - # - # + return - A list of tools available on the server, or an `mcp:Error` on failure. - isolated remote function listTools() returns ListToolsResult|error { - ListToolsRequest listToolRequest = { + # Retrieves the list of available tools from the server. + # + # + return - List of available tools or a ClientError. + isolated remote function listTools() returns ListToolsResult|ClientError { + ListToolsRequest listToolsRequest = { method: "tools/list" }; - JsonRpcMessage|stream|() response = check self.sendRequest(listToolRequest); - ListToolsResult listToolResult = check handleListToolResult(response); - return listToolResult; + + ServerResult result = check self.sendRequestMessage(listToolsRequest); + if result is ListToolsResult { + return result; + } else { + return error ListToolsError( + string `Tool listing failed: unexpected result type '${(typeof result).toString()}' received.` + ); + } } - # Calls a tool on the MCP server with the specified parameters. - # - # + params - The parameters for the tool call, including the tool name and arguments. - # + return - The result of the tool call, or an `mcp:Error` on failure. - isolated remote function callTool(CallToolParams params) returns JsonRpcMessage|stream|error { - CallToolRequest callToolRequest = { + # Executes a tool on the server with the given parameters. + # + # + params - Tool execution parameters, including name and arguments. + # + return - Result of the tool execution or a ClientError. + isolated remote function callTool(CallToolParams params) returns CallToolResult|ClientError { + CallToolRequest toolCallRequest = { method: "tools/call", params: params }; - JsonRpcMessage|stream|() response = check self.sendRequest(callToolRequest); - if response is () { - return error UninitializedTransportError("Failed to initialize transport for MCP client"); - // return error CallToolError("Received invalid response for tool call"); + + ServerResult result = check self.sendRequestMessage(toolCallRequest); + if result is CallToolResult { + return result; + } else { + return error ToolCallError( + string `Tool call failed: unexpected result type '${(typeof result).toString()}' received.` + ); } - return response; } - # Closes the MCP client and terminates the transport session. - # - # + return - nil on success, or an `mcp:Error` on failure. - isolated remote function close() returns error? { - StreamableHttpClientTransport? transport = self.transport; - if transport is () { - return error UninitializedTransportError("Transport is not initialized for sending requests"); + # Closes the session and disconnects from the server. + # + # + return - A ClientError if closure fails, or nil on success. + isolated remote function close() returns ClientError? { + StreamableHttpClientTransport? currentTransport = self.transport; + if currentTransport is () { + return error UninitializedTransportError( + "Closure failed: client transport is not initialized. Call initialize() first." + ); } - return transport.terminateSession(); + do { + check currentTransport.terminateSession(); + lock { + self.transport = (); + self.serverCapabilities = (); + self.serverInfo = (); + } + return; + } on fail error e { + return error ClientError(string `Failed to disconnect from server: ${e.message()}`, e); + } } - private isolated function sendRequest(Request request) returns JsonRpcMessage|stream|error? { - StreamableHttpClientTransport? transport = self.transport; - if transport is () { - return error UninitializedTransportError("Streamable HTTP transport is not initialized for sending requests"); + # Sends a request message to the server and returns the server's response. + # + # + request - The request object to send. + # + return - ServerResult, a stream of results, or a ClientError. + private isolated function sendRequestMessage(Request request) returns ServerResult|ClientError { + StreamableHttpClientTransport? currentTransport = self.transport; + if currentTransport is () { + return error UninitializedTransportError( + "Cannot send request: client transport is not initialized. Call initialize() first." + ); } - self.requestMessageId += 1; - JsonRpcRequest jsonrpcRequest = { - ...request, - jsonrpc: JSONRPC_VERSION, - id: self.requestMessageId - }; + lock { + self.requestId += 1; - return transport.send(jsonrpcRequest); + JsonRpcRequest jsonRpcRequest = { + ...request, + jsonrpc: JSONRPC_VERSION, + id: self.requestId + }; + + JsonRpcMessage|stream|StreamableHttpTransportError? response = + currentTransport.sendMessage(jsonRpcRequest); + return processServerResponse(response); + } } - private isolated function sendNotification(Notification notification) returns error? { - StreamableHttpClientTransport? transport = self.transport; - if transport is () { - return error UninitializedTransportError("Streamable HTTP transport is not initialized for sending requests"); + # Sends a notification message to the server. + # + # + notification - The notification object to send. + # + return - A ClientError if sending fails, or nil on success. + private isolated function sendNotificationMessage(Notification notification) returns ClientError? { + StreamableHttpClientTransport? currentTransport = self.transport; + if currentTransport is () { + return error UninitializedTransportError( + "Cannot send notification: client transport is not initialized. Call initialize() first." + ); } - JsonRpcNotification jsonrpcNotification = { + JsonRpcNotification jsonRpcNotification = { ...notification, jsonrpc: JSONRPC_VERSION }; - _ = check transport.send(jsonrpcNotification); + _ = check currentTransport.sendMessage(jsonRpcNotification); } } diff --git a/ballerina/error.bal b/ballerina/error.bal index e038786..3d83bcb 100644 --- a/ballerina/error.bal +++ b/ballerina/error.bal @@ -14,29 +14,71 @@ // specific language governing permissions and limitations // under the License. -# Defines the common error type for the module. +# Defines the common base error type for this module. public type Error distinct error; -# Errors related to streaming operations. -public type StreamError distinct Error; +# Error for failures during streaming operations. +public type StreamError distinct Error & ClientError; -# Errors related to processing JSON-RPC message streams. -public type JsonRpcMessageStreamError distinct StreamError; +# Error for failures during transport operations. +public type TransportError distinct Error; + +# Error for invalid or unexpected responses from the server. +public type ServerResponseError distinct Error & ClientError; -# Errors related to client. +# Error for failures occurring within client operations. public type ClientError distinct Error; -# Errors related to transport operations. -public type TransportError distinct Error; +# Error for failures while processing SSE event streams. +public type SseEventStreamError distinct StreamError; -# Errors related to initialization of the client. -public type ClientInitializationError distinct ClientError; +# Error for JSON-RPC message transformation failures during streaming. +public type JsonRpcMessageTransformationError distinct StreamError; + +# Error when required data is missing from an SSE event. +public type MissingSseDataError distinct JsonRpcMessageTransformationError; + +# Error for JSON parsing failures within SSE event data. +public type JsonParsingError distinct JsonRpcMessageTransformationError; + +# Error for failures converting JSON to JsonRpcMessage. +public type TypeConversionError distinct JsonRpcMessageTransformationError; + +# Error when an invalid message type is received from the server. +public type InvalidMessageTypeError distinct ServerResponseError; + +# Error when the server response is malformed or unexpected. +public type MalformedResponseError distinct ServerResponseError; -# Errors related to uninitialized transport. -public type UninitializedTransportError distinct TransportError; +# Error for failures during HTTP transport operations. +public type StreamableHttpTransportError distinct TransportError & ClientError; -# Errors related to streamable HTTP transport operations. -public type StreamableHttpTransportError distinct TransportError; +# Error for failures during HTTP client operations. +public type HttpClientError distinct StreamableHttpTransportError; -# Errors due to unsupported content type. +# Error for unsupported content types in HTTP responses. public type UnsupportedContentTypeError distinct StreamableHttpTransportError; + +# Error for failures during session operations. +public type SessionOperationError distinct StreamableHttpTransportError; + +# Error for failures while parsing HTTP response content. +public type ResponseParsingError distinct StreamableHttpTransportError; + +# Error for failures during SSE stream establishment. +public type SseStreamEstablishmentError distinct StreamableHttpTransportError; + +# Error for operations attempted before transport initialization. +public type UninitializedTransportError distinct ClientError; + +# Error for failures during client initialization. +public type ClientInitializationError distinct ClientError; + +# Error for protocol version negotiation failures. +public type ProtocolVersionError distinct ClientInitializationError; + +# Error for failures during tool listing operations. +public type ListToolsError distinct ClientError; + +# Error for failures during tool execution operations. +public type ToolCallError distinct ClientError; diff --git a/ballerina/init.bal b/ballerina/init.bal new file mode 100644 index 0000000..fb3da35 --- /dev/null +++ b/ballerina/init.bal @@ -0,0 +1,25 @@ +// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/jballerina.java; + +isolated function init() { + setModule(); +} + +isolated function setModule() = @java:Method { + 'class: "io.ballerina.stdlib.mcp.ModuleUtils" +} external; diff --git a/ballerina/json_rpc_message_stream_transformer.bal b/ballerina/json_rpc_message_stream_transformer.bal new file mode 100644 index 0000000..318effd --- /dev/null +++ b/ballerina/json_rpc_message_stream_transformer.bal @@ -0,0 +1,112 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; +import ballerina/jballerina.java; + +# Transforms a stream of SSE events into a stream of JsonRpcMessages. +isolated class JsonRpcMessageStreamTransformer { + + # Initializes the transformer with an SSE event stream. + # + # + sseEventStream - The SSE event stream to use as input. + public isolated function init(stream sseEventStream) { + self.attachSseStream(sseEventStream); + } + + # Retrieves the next JsonRpcMessage from the SSE event stream. + # + # + return - A record with the next JsonRpcMessage, a StreamError, or nil if the stream is complete. + public isolated function next() returns record {|JsonRpcMessage value;|}|StreamError? { + record {|http:SseEvent value;|}|error? sseEventRecord = self.getNextSseEvent(); + + if sseEventRecord is () { + return; // End of stream. + } + + if sseEventRecord is error { + return error SseEventStreamError( + string `Failed to retrieve SSE event: ${sseEventRecord.message()}` + ); + } + + string? eventData = sseEventRecord.value.data; + JsonRpcMessage|JsonRpcMessageTransformationError jsonRpcMessage = self.convertSseDataToJsonRpcMessage(eventData); + + if jsonRpcMessage is JsonRpcMessageTransformationError { + return jsonRpcMessage; + } + + return { + value: jsonRpcMessage + }; + } + + # Closes the underlying SSE event stream. + # + # + return - A StreamError if closing fails, or nil if successful. + public isolated function close() returns StreamError? { + error? closeError = self.closeSseEventStream(); + if closeError is error { + return error SseEventStreamError(string `Failed to close SSE event stream: ${closeError.message()}`); + } + return; + } + + # Attaches the SSE event stream to this transformer instance. + # + # + sseEventStream - The stream of SSE events to bind. + private isolated function attachSseStream(stream sseEventStream) = @java:Method { + 'class: "io.ballerina.stdlib.mcp.SseEventStreamHelper" + } external; + + # Retrieves the next event from the SSE stream. + # + # + return - Record containing the next SSE event, error, or nil if the stream is complete. + private isolated function getNextSseEvent() returns record {|http:SseEvent value;|}?|error? = @java:Method { + 'class: "io.ballerina.stdlib.mcp.SseEventStreamHelper" + } external; + + # Closes the attached SSE event stream. + # + # + return - Error if closing fails, or nil if successful. + private isolated function closeSseEventStream() returns error? = @java:Method { + 'class: "io.ballerina.stdlib.mcp.SseEventStreamHelper" + } external; + + # Converts SSE event data to a JsonRpcMessage. + # + # + eventData - The `data` field from an SSE event. + # + return - A JsonRpcMessage or a JsonRpcMessageTransformationError. + private isolated function convertSseDataToJsonRpcMessage(string? eventData) returns JsonRpcMessage|JsonRpcMessageTransformationError { + if eventData is () { + return error MissingSseDataError("SSE event is missing the required 'data' field."); + } + + json jsonData; + do { + jsonData = check eventData.fromJsonString(); + } on fail error e { + return error JsonParsingError(string `Failed to parse SSE event data as JSON: ${e.message()}`); + } + + do { + return check jsonData.cloneWithType(); + } on fail error e { + return error TypeConversionError(string `Failed to convert JSON data to JsonRpcMessage: ${e.message()}`); + } + } +} diff --git a/ballerina/main.bal b/ballerina/main.bal deleted file mode 100644 index cfae47f..0000000 --- a/ballerina/main.bal +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import ballerina/io; - -Client mcpClient = new ("http://localhost:3000/mcp", {"name": "MCP Client", "version": "1.0.0"}); -record { - string name; - string description?; -}[] tools = []; - -// Change to remove functions in the client -public function main() returns error? { - check mcpClient->initialize(); - // stream serverMessages = check mcpClient.subscribeToServerMessages(); - // check from JSONRPCServerMessage serverMessage in serverMessages - // do { - // io:println("Received server message: ", serverMessage); - // }; - check listTools(); - // foreach var tool in tools { - // check callTool(tool.name); - // } - // // let's introduce close and let the user implement this. - // check mcpClient.waitForCompletion(); -} - -function listTools() returns error? { - ListToolsResult toolsResult = check mcpClient->listTools(); - foreach Tool tool in toolsResult.tools { - io:println("Tool Name: ", tool.name); - tools.push({ - name: tool.name, - description: tool.description - }); - } -} - -// function callTool(string name) returns error? { -// io:println("\nCalling tool: " + name); -// JSONRPCServerMessage|stream result = check mcpClient.callTool({ -// name: name, -// arguments: {"name": "itsuki"} -// }); -// if result is JSONRPCServerMessage { -// if result is JSONRPCResponse { -// ServerResult serverResult = result.result; -// if serverResult is CallToolResult { -// io:println("Tool call result: ", serverResult); -// } else { -// return error CallToolError("Received unexpected response type for tool call: " + serverResult.toString()); -// } -// } else if result is JSONRPCNotification { -// io:println("Received notification for tool call: ", result); -// } -// } else if result is stream { -// check from JSONRPCServerMessage serverMessage in 'result -// do { -// check handleMessage(serverMessage); -// }; -// } -// } - -// function executeWithDefaultValues(JSONRPCServerMessage notif) returns error? { -// io:println("Received notification: ", notif); -// } - -// function handleMessage(JSONRPCServerMessage serverMessage) returns error? { -// if serverMessage is JSONRPCResponse { -// io:println("Received message for tool call: ", serverMessage); -// } else if serverMessage is JSONRPCNotification { -// io:println("Received notification for tool call: ", serverMessage); -// } else if serverMessage is JSONRPCError { -// io:println("Received error for tool call: ", serverMessage); -// } else { -// return error CallToolError("Received unexpected response type for tool call: " + serverMessage.toString()); -// } -// } diff --git a/ballerina/message_event_stream_gen.bal b/ballerina/message_event_stream_gen.bal deleted file mode 100644 index 6a5a16b..0000000 --- a/ballerina/message_event_stream_gen.bal +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import ballerina/http; -import ballerina/jballerina.java; - -isolated class MessageEventStreamGenerator { - - public isolated function init(stream eventStream) { - self.externInit(eventStream); - } - - private isolated function externInit(stream eventStream) = @java:Method { - 'class: "io.ballerina.stdlib.mcp.MessageEventStream", - name: "initialize" - } external; - - isolated function getNextData() returns record {|http:SseEvent value;|}?|error? = @java:Method { - 'class: "io.ballerina.stdlib.mcp.MessageEventStream" - } external; - - public isolated function next() returns record {|JsonRpcMessage value;|}|error? { - record {|http:SseEvent value;|}? recordVal = check self.getNextData(); - - // If End of Stream - if recordVal is () { - return (); - } - - string? data = recordVal.value.data; - if data is () { - return error JsonRpcMessageStreamError("Received SSE event without 'data' field in the event stream."); - } - - json jsonData = check data.fromJsonString(); - JsonRpcMessage rpcResponse = check jsonData.cloneWithType(); - return { - value: rpcResponse - }; - }; - - public isolated function close() returns error? { - // check self.eventStream.close(); - } -} diff --git a/ballerina/protocol.bal b/ballerina/protocol.bal index be21ed7..b4ea5c4 100644 --- a/ballerina/protocol.bal +++ b/ballerina/protocol.bal @@ -17,12 +17,12 @@ # Additional initialization options. # # + enforceStrictCapabilities - Whether to restrict emitted requests to only those that the remote -# side has indicated that they can handle, through their advertised -# capabilities. Note that this DOES NOT affect checking of _local_ -# side capabilities, as it is considered a logic error to mis-specify -# those. Currently this defaults to false, for backwards compatibility -# with SDK versions that did not advertise capabilities correctly. In -# future, this will default to true. +# side has indicated that they can handle, through their advertised +# capabilities. Note that this DOES NOT affect checking of _local_ +# side capabilities, as it is considered a logic error to mis-specify +# those. Currently this defaults to false, for backwards compatibility +# with SDK versions that did not advertise capabilities correctly. In +# future, this will default to true. public type ProtocolOptions record {| boolean enforceStrictCapabilities?; |}; diff --git a/ballerina/streamable_http.bal b/ballerina/streamable_http.bal index 0e6163f..81bf4ff 100644 --- a/ballerina/streamable_http.bal +++ b/ballerina/streamable_http.bal @@ -16,101 +16,170 @@ import ballerina/http; -type StreamableHttpClientTransportOptions record {| +# Configuration options for the Streamable HTTP client transport. +# +# + sessionId - Optional session identifier for continued interactions. +type StreamableHttpClientTransportConfig record {| string? sessionId = (); |}; +# Provides HTTP-based client transport with support for streaming. isolated class StreamableHttpClientTransport { - private final string url; + private final string serverUrl; private final http:Client httpClient; private string? sessionId; - isolated function init(string url, StreamableHttpClientTransportOptions? options = ()) returns error? { - self.url = url; - self.httpClient = check new (url); - self.sessionId = options?.sessionId; + # Initializes the HTTP client transport with the provided server URL. + # + # + serverUrl - The URL of the server endpoint. + # + config - Optional configuration, such as session ID. + # + return - A StreamableHttpTransportError if initialization fails; otherwise, nil. + isolated function init(string serverUrl, StreamableHttpClientTransportConfig? config = ()) returns StreamableHttpTransportError? { + self.serverUrl = serverUrl; + do { + self.httpClient = check new (serverUrl); + } on fail error e { + return error HttpClientError(string `Unable to initialize HTTP client for '${serverUrl}': ${e.message()}`); + } + self.sessionId = config?.sessionId; } - isolated function send(JsonRpcMessage message) returns JsonRpcMessage|stream|error? { - map headers = self.commonHeaders(); + # Sends a JSON-RPC message to the server and returns the response. + # + # + message - The JSON-RPC message to send. + # + return - A JSON-RPC response message, a stream of messages, or a transport error. + isolated function sendMessage(JsonRpcMessage message) returns JsonRpcMessage|stream|StreamableHttpTransportError? { + map headers = self.prepareRequestHeaders(); headers[CONTENT_TYPE_HEADER] = CONTENT_TYPE_JSON; headers[ACCEPT_HEADER] = string `${CONTENT_TYPE_JSON}, ${CONTENT_TYPE_SSE}`; - http:Response response = check self.httpClient->post("/", message, headers = headers); + do { + http:Response response = check self.httpClient->post("/", message, headers = headers); - // Handle sessionId during the initialization request - string|error sessionId = response.getHeader(SESSION_ID_HEADER); - lock { - if sessionId is string { - self.sessionId = sessionId; + // Handle session ID in the initialization response. + string|error sessionIdHeader = response.getHeader(SESSION_ID_HEADER); + if sessionIdHeader is string { + lock { + self.sessionId = sessionIdHeader; + } } - } - // If the response is 202 Accepted, there's no body to process - if response.statusCode == 202 { - return; - } + // If response is 202 Accepted, there is no content to process. + if response.statusCode == 202 { + return; + } - // Handle the response based on the content type - string contentType = response.getContentType(); - if contentType.includes(CONTENT_TYPE_SSE) { - stream sseEventStream = check response.getSseEventStream(); - MessageEventStreamGenerator msgEventStreamGenerator = new (sseEventStream); - stream msgEventStream = new (msgEventStreamGenerator); - return msgEventStream; - } else if contentType.includes(CONTENT_TYPE_JSON) { - json jsonPayload = check response.getJsonPayload(); - JsonRpcMessage serverMsg = check jsonPayload.cloneWithType(); - return serverMsg; - } else { - return error UnsupportedContentTypeError("Unsupported content type: " + contentType); + // Dispatch response based on content type. + string contentType = response.getContentType(); + if contentType.includes(CONTENT_TYPE_SSE) { + return self.processServerSentEvents(response); + } else if contentType.includes(CONTENT_TYPE_JSON) { + return self.processJsonResponse(response); + } else { + return error UnsupportedContentTypeError( + string `Server returned unsupported content type '${contentType}'.` + ); + } + } on fail error e { + return error HttpClientError(string `Failed to send message to server: ${e.message()}`); } } - isolated function startSse() returns stream|error { - map headers = self.commonHeaders(); - headers[ACCEPT_HEADER] = "text/event-stream"; - - stream sseEventStream = check self.httpClient->get("/", headers = headers); - MessageEventStreamGenerator msgEventStreamGenerator = new (sseEventStream); - stream msgEventStream = new (msgEventStreamGenerator); - return msgEventStream; + # Establishes a Server-Sent Events (SSE) stream with the server. + # + # + return - A stream of JsonRpcMessages, or a StreamableHttpTransportError. + isolated function establishEventStream() returns stream|StreamableHttpTransportError { + map headers = self.prepareRequestHeaders(); + headers[ACCEPT_HEADER] = CONTENT_TYPE_SSE; + + do { + stream sseEventStream = check self.httpClient->get("/", headers = headers); + + JsonRpcMessageStreamTransformer streamTransformer = new (sseEventStream); + return new stream(streamTransformer); + } on fail error e { + return error SseStreamEstablishmentError( + string `Failed to establish SSE connection with server: ${e.message()}` + ); + } } - isolated function terminateSession() returns error? { + # Terminates the current session with the server. + # + # + return - A StreamableHttpTransportError if termination fails; otherwise, nil. + isolated function terminateSession() returns StreamableHttpTransportError? { lock { - if (self.sessionId is ()) { + if self.sessionId is () { return; } - map headers = self.commonHeaders(); + map headers = self.prepareRequestHeaders(); headers[CONTENT_TYPE_HEADER] = CONTENT_TYPE_JSON; - http:Response response = check self.httpClient->delete("/", headers = headers); + do { + http:Response response = check self.httpClient->delete("/", headers = headers); - if response.statusCode == 405 { - return error TransportError("Session termination not supported by the server"); - } + if response.statusCode == 405 { + return error SessionOperationError("Server does not support session termination."); + } - self.sessionId = (); - return; + self.sessionId = (); + return; + } on fail error e { + return error SessionOperationError( + string `Failed to terminate session: ${e.message()}` + ); + } } } + # Returns the current session ID, or nil if no session is active. + # + # + return - The current session ID as a string, or nil if not set. isolated function getSessionId() returns string? { lock { return self.sessionId; } } - private isolated function commonHeaders() returns map { + # Prepares common HTTP headers for requests, including the session ID if present. + # + # + return - Map of common headers to include in each request. + private isolated function prepareRequestHeaders() returns map { lock { - map headers = {}; - string? sessionId = self.sessionId; - if (sessionId is string) { - headers[SESSION_ID_HEADER] = sessionId; - } - return headers.clone(); + string? currentSessionId = self.sessionId; + return currentSessionId is string ? {[SESSION_ID_HEADER]: currentSessionId} : {}; + } + } + + # Processes a Server-Sent Events HTTP response into a stream of JsonRpcMessages. + # + # + response - The HTTP response containing SSE data. + # + return - A stream of JsonRpcMessages, or a StreamableHttpTransportError. + private isolated function processServerSentEvents(http:Response response) returns stream|StreamableHttpTransportError { + do { + stream sseEventStream = check response.getSseEventStream(); + JsonRpcMessageStreamTransformer streamTransformer = new (sseEventStream); + return new stream(streamTransformer); + } on fail error e { + return error ResponseParsingError( + string `Unable to process SSE response: ${e.message()}` + ); + } + } + + # Processes a JSON HTTP response into a JsonRpcMessage. + # + # + response - The HTTP response containing JSON data. + # + return - A JsonRpcMessage, or a StreamableHttpTransportError. + private isolated function processJsonResponse(http:Response response) returns JsonRpcMessage|StreamableHttpTransportError { + do { + json payload = check response.getJsonPayload(); + return check payload.cloneWithType(); + } on fail error e { + return error ResponseParsingError( + string `Unable to parse JSON response: ${e.message()}` + ); } } } diff --git a/ballerina/types.bal b/ballerina/types.bal index b9a98cf..fb930ef 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -19,9 +19,9 @@ public type JsonRpcMessage JsonRpcRequest|JsonRpcNotification|JsonRpcResponse|Js public const LATEST_PROTOCOL_VERSION = "2025-03-26"; public const SUPPORTED_PROTOCOL_VERSIONS = [ - LATEST_PROTOCOL_VERSION, - "2024-11-05", - "2024-10-07" + LATEST_PROTOCOL_VERSION, + "2024-11-05", + "2024-10-07" ]; public const JSONRPC_VERSION = "2.0"; @@ -251,7 +251,7 @@ public type Implementation record {| # A ping, issued by either the server or the client, to check that # the other party is still alive. The receiver must promptly respond, # or else may be disconnected. -# +# # + method - The method name public type PingRequest record {| *Request; @@ -259,7 +259,7 @@ public type PingRequest record {| |}; # An out-of-band notification used to inform the receiver of a progress update for a long-running request. -# +# # + method - The method name for the notification # + params - The parameters for the progress notification public type ProgressNotification record {| @@ -292,7 +292,7 @@ public type PaginatedRequest record {| |}; # Result that supports pagination -# +# # + nextCursor - An opaque token representing the pagination position after the last returned result. # If present, there may be more results available. public type PaginatedResult record {| @@ -302,7 +302,7 @@ public type PaginatedResult record {| # An optional notification from the server to the client, informing it that the list of resources it can read from has changed. # This may be issued by servers without any previous subscription from the client. -# +# # + method - The JSON-RPC method name for resource list changed notifications public type ResourceListChangedNotification record {| *Notification; @@ -311,7 +311,7 @@ public type ResourceListChangedNotification record {| # A notification from the server to the client, informing it that a resource has changed and may need to be read again. # This should only be sent if the client previously sent a resources/subscribe request. -# +# # + method - The JSON-RPC method name for resource updated notifications public type ResourceUpdatedNotification record {| *Notification; @@ -326,7 +326,7 @@ public type ResourceUpdatedNotification record {| |}; # The contents of a specific resource or sub-resource. -# +# # + uri - The URI of this resource. # + mimeType - The MIME type of this resource, if known. public type ResourceContents record {| @@ -335,7 +335,7 @@ public type ResourceContents record {| |}; # Text resource contents -# +# # + text - The text of the item. This must only be set if the item can actually be represented as text (not binary data). public type TextResourceContents record {| *ResourceContents; @@ -343,7 +343,7 @@ public type TextResourceContents record {| |}; # Binary resource contents -# +# # + blob - A base64-encoded string representing the binary data of the item. public type BlobResourceContents record {| *ResourceContents; @@ -357,7 +357,7 @@ public type Role "user"|"assistant"; # # It is up to the client how best to render embedded resources for the benefit # of the LLM and/or the user. -# +# # + type - The type of content # + resource - The resource content # + annotations - Optional annotations for the client @@ -370,7 +370,7 @@ public type EmbeddedResource record {| # An optional notification from the server to the client, informing it that # the list of prompts it offers has changed. This may be issued by servers # without any previous subscription from the client. -# +# # + method - The JSON-RPC method name for prompt list changed notifications public type PromptListChangedNotification record {| *Notification; @@ -386,7 +386,7 @@ public type ListToolsRequest record {| |}; # The server's response to a tools/list request from the client. -# +# # + tools - A list of tools available on the server. public type ListToolsResult record {| *PaginatedResult; @@ -394,16 +394,16 @@ public type ListToolsResult record {| |}; # The server's response to a tool call. -# +# # Any errors that originate from the tool SHOULD be reported inside the result # object, with `isError` set to true, _not_ as an MCP protocol-level error # response. Otherwise, the LLM would not be able to see that an error occurred # and self-correct. -# +# # However, any errors in _finding_ the tool, an error indicating that the # server does not support tool calls, or any other exceptional conditions, # should be reported as an MCP error response. -# +# # + content - The content of the tool call result # + isError - Whether the tool call ended in an error. # If not set, this is assumed to be false (the call was successful). @@ -432,7 +432,7 @@ public type CallToolParams record {| # An optional notification from the server to the client, informing it that the list of tools # it offers has changed. This may be issued by servers without any previous subscription from the client. -# +# # + method - The JSON-RPC method name for tool list changed notifications public type ToolListChangedNotification record {| *Notification; @@ -445,7 +445,7 @@ public type ToolListChangedNotification record {| # tool behavior (including descriptive properties like `title`). # Clients should never make tool use decisions based on ToolAnnotations # received from untrusted servers. -# +# # + title - A human-readable title for the tool. # + readOnlyHint - If true, the tool does not modify its environment. # Default: false @@ -471,7 +471,7 @@ public type ToolAnnotations record {| |}; # Definition for a tool the client can call. -# +# # + name - The name of the tool # + description - A human-readable description of the tool # This can be used by clients to improve the LLM's understanding of available tools. @@ -490,7 +490,7 @@ public type Tool record {| # Notification of a log message passed from server to client. If no logging/setLevel request has been # sent from the client, the server MAY decide which messages to send automatically. -# +# # + method - The method name for the notification # + params - The parameters for the logging message notification public type LoggingMessageNotification record {| @@ -526,7 +526,7 @@ public type Annotations record {| |}; # Text provided to or from an LLM. -# +# # + type - The type of content # + text - The text content of the message # + annotations - Optional annotations for the client @@ -537,7 +537,7 @@ public type TextContent record {| |}; # An image provided to or from an LLM. -# +# # + type - The type of content # + data - The base64-encoded image data # + mimeType - The MIME type of the image. Different providers may support different image types. @@ -550,7 +550,7 @@ public type ImageContent record {| |}; # Audio provided to or from an LLM. -# +# # + type - The type of content # + data - The base64-encoded audio data # + mimeType - The MIME type of the audio. Different providers may support different audio types. @@ -564,10 +564,10 @@ public type AudioContent record {| // Client messages # Represents a request sent from the client to the server. -public type ClientRequest PingRequest | InitializeRequest | CallToolRequest | ListToolsRequest; +public type ClientRequest PingRequest|InitializeRequest|CallToolRequest|ListToolsRequest; # Represents a notification sent from the client to the server. -public type ClientNotification CancelledNotification | ProgressNotification | InitializedNotification; +public type ClientNotification CancelledNotification|ProgressNotification|InitializedNotification; # Represents a result sent from the client to the server. public type ClientResult EmptyResult; @@ -578,12 +578,12 @@ public type ServerRequest PingRequest; # Represents a notification sent from the server to the client. public type ServerNotification CancelledNotification - | ProgressNotification - | LoggingMessageNotification - | ResourceUpdatedNotification - | ResourceListChangedNotification - | ToolListChangedNotification - | PromptListChangedNotification; + |ProgressNotification + |LoggingMessageNotification + |ResourceUpdatedNotification + |ResourceListChangedNotification + |ToolListChangedNotification + |PromptListChangedNotification; # Represents a result sent from the server to the client. -public type ServerResult InitializeResult | CallToolResult | ListToolsResult | EmptyResult; +public type ServerResult InitializeResult|CallToolResult|ListToolsResult|EmptyResult; diff --git a/ballerina/utils.bal b/ballerina/utils.bal index 8d9fe96..2b03502 100644 --- a/ballerina/utils.bal +++ b/ballerina/utils.bal @@ -14,60 +14,64 @@ // specific language governing permissions and limitations // under the License. -isolated function handleInitializeResponse(JsonRpcMessage|stream|() response) returns InitializeResult|error { - if response is stream { - (record {|JsonRpcMessage value;|}|error)? message = response.next(); - if message is () { - // return error ClientInitializationError("Failed to receive an initialize response"); - return error("Error"); - } - if message is error { - return message; - } - JsonRpcMessage serverMessage = message.value; - if serverMessage is JsonRpcResponse { - ServerResult result = serverMessage.result; - if result is InitializeResult { - return result; - } - } - } else if response is JsonRpcMessage { - if response is JsonRpcResponse { - ServerResult result = response.result; - if result is InitializeResult { - return result; - } - } +# Processes a server response and extracts the result. +# +# + serverResponse - The response from the server, which may be a single JsonRpcMessage, a stream, or a transport error. +# + return - Extracted ServerResult, ServerResponseError, or StreamError. +isolated function processServerResponse(JsonRpcMessage|stream|StreamableHttpTransportError? serverResponse) + returns ServerResult|ServerResponseError|StreamError { + + // If response is a stream, extract the result from the stream. + if serverResponse is stream { + return extractResultFromMessageStream(serverResponse); } - return error("Error"); - // return error ClientInitializationError("No response received for initialization"); -} -isolated function handleListToolResult(JsonRpcMessage|stream|() response) returns ListToolsResult|error { - if response is stream { - (record {|JsonRpcMessage value;|}|error)? message = response.next(); - if message is () { - // return error ListToolError("Failed to receive a list tools response"); - return error("Error"); - } - if message is error { - return message; - } - JsonRpcMessage serverMessage = message.value; - if serverMessage is JsonRpcResponse { - ServerResult result = serverMessage.result; - if result is ListToolsResult { - return result; - } - } - } else if response is JsonRpcMessage { - if response is JsonRpcResponse { - ServerResult result = response.result; - if result is ListToolsResult { - return result; - } + // If response is a direct JsonRpcMessage, convert it to a result. + if serverResponse is JsonRpcMessage { + return convertMessageToResult(serverResponse); + } + + // Null response: indicates malformed or missing server reply. + if serverResponse is () { + return error MalformedResponseError("Received null response from server."); + } + + // If a transport error is returned, wrap as a ServerResponseError. + if serverResponse is StreamableHttpTransportError { + return error ServerResponseError( + string `Transport error connecting to server: ${serverResponse.message()}` + ); + } +} + +# Extracts the first valid result from a stream of JsonRpcMessages. +# +# + messageStream - The stream of JsonRpcMessages to process. +# + return - The first valid ServerResult, a specific ServerResponseError, or StreamError. +isolated function extractResultFromMessageStream(stream messageStream) + returns ServerResult|ServerResponseError|StreamError { + + record {|JsonRpcMessage value;|}|StreamError? streamItem = messageStream.next(); + // Iterate until a valid result or an error is found. + while streamItem !is () { + if streamItem is StreamError { + return streamItem; } + + JsonRpcMessage message = streamItem.value; + return convertMessageToResult(message); + } + + return error InvalidMessageTypeError("No valid messages found in server message stream."); +} + +# Converts a JsonRpcMessage to a ServerResult. +# +# + message - The JsonRpcMessage to convert. +# + return - The extracted ServerResult, or an InvalidMessageTypeError. +isolated function convertMessageToResult(JsonRpcMessage message) returns ServerResult|ServerResponseError { + if message is JsonRpcResponse { + return message.result; } - return error("Error"); - // return error ListToolError("No response received for list tools"); + return error InvalidMessageTypeError("Received message from server is not a valid JsonRpcResponse."); } diff --git a/native/src/main/java/io/ballerina/stdlib/mcp/MessageEventStream.java b/native/src/main/java/io/ballerina/stdlib/mcp/ModuleUtils.java similarity index 58% rename from native/src/main/java/io/ballerina/stdlib/mcp/MessageEventStream.java rename to native/src/main/java/io/ballerina/stdlib/mcp/ModuleUtils.java index 9ef454f..ba09fe7 100644 --- a/native/src/main/java/io/ballerina/stdlib/mcp/MessageEventStream.java +++ b/native/src/main/java/io/ballerina/stdlib/mcp/ModuleUtils.java @@ -19,17 +19,21 @@ package io.ballerina.stdlib.mcp; import io.ballerina.runtime.api.Environment; -import io.ballerina.runtime.api.values.BObject; -import io.ballerina.runtime.api.values.BStream; +import io.ballerina.runtime.api.Module; -public class MessageEventStream { - public static void initialize(BObject object, BStream eventStream) { - object.addNativeData("stream", eventStream); +public final class ModuleUtils { + private static Module module; + + private ModuleUtils() { + } + + @SuppressWarnings("unused") + public static Module getModule() { + return module; } - public static Object getNextData(Environment env, BObject object) { - BStream eventStream = (BStream) object.getNativeData("stream"); - BObject iteratorObj = eventStream.getIteratorObj(); - return env.getRuntime().callMethod(iteratorObj, "next", null); + @SuppressWarnings("unused") + public static void setModule(Environment env) { + module = env.getCurrentModule(); } } diff --git a/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java b/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java new file mode 100644 index 0000000..baa407e --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.mcp; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BStream; + +/** + * Utility class for handling Server-Sent Events (SSE) streams in Ballerina via Java interop. + *

+ * Provides static helper methods to: + *

    + *
  • Attach an SSE stream to a Ballerina object as native data.
  • + *
  • Retrieve the next event from an SSE stream.
  • + *
  • Close the SSE stream and release resources.
  • + *
+ * This class is not instantiable. + */ +public final class SseEventStreamHelper { + + /** Native data key used to store the SSE stream in the Ballerina object. */ + private static final String SSE_STREAM_NATIVE_KEY = "sseStream"; + + // Private constructor to prevent instantiation. + private SseEventStreamHelper() {} + + /** + * Attaches the provided SSE {@link BStream} as native data to the specified Ballerina object. + * + * @param object The Ballerina object that will hold the SSE stream (native data). + * @param sseStream The SSE stream instance to attach. + */ + public static void attachSseStream(BObject object, BStream sseStream) { + object.addNativeData(SSE_STREAM_NATIVE_KEY, sseStream); + } + + /** + * Retrieves the next event from the SSE stream attached to the given Ballerina object. + *

+ * Invokes the "next" method on the stream's iterator object using the Ballerina runtime. + * + * @param env The Ballerina runtime environment. + * @param object The Ballerina object holding the SSE stream as native data. + * @return The next SSE event record, or null if the stream is exhausted. + * @throws IllegalStateException if the SSE stream has not been attached to the object. + */ + public static Object getNextSseEvent(Environment env, BObject object) { + BStream sseStream = (BStream) object.getNativeData(SSE_STREAM_NATIVE_KEY); + if (sseStream == null) { + throw new IllegalStateException("SSE stream not attached to the provided Ballerina object."); + } + BObject iteratorObject = sseStream.getIteratorObj(); + // Use the Ballerina runtime to call the "next" method on the iterator and fetch the next event. + return env.getRuntime().callMethod(iteratorObject, "next", null); + } + + /** + * Closes the SSE stream attached to the given Ballerina object. + *

+ * Invokes the "close" method on the stream's iterator object using the Ballerina runtime. + * + * @param env The Ballerina runtime environment. + * @param object The Ballerina object holding the SSE stream as native data. + * @return The result of the close operation (could be null or error). + * @throws IllegalStateException if the SSE stream has not been attached to the object. + */ + public static Object closeSseEventStream(Environment env, BObject object) { + BStream sseStream = (BStream) object.getNativeData(SSE_STREAM_NATIVE_KEY); + if (sseStream == null) { + throw new IllegalStateException("SSE stream not attached to the provided Ballerina object."); + } + BObject iteratorObject = sseStream.getIteratorObj(); + // Use the Ballerina runtime to call the "close" method on the iterator and release resources. + return env.getRuntime().callMethod(iteratorObject, "close", null); + } +} From ad33751e099fced6fb774d9093d2f3ff49d1c36f Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Wed, 4 Jun 2025 13:42:52 +0530 Subject: [PATCH 08/15] Add missing last line --- native/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/build.gradle b/native/build.gradle index eb2eeb2..f7249ac 100644 --- a/native/build.gradle +++ b/native/build.gradle @@ -84,4 +84,4 @@ compileJava { ] classpath = files() } -} \ No newline at end of file +} From f23836ec67e623a3417c7a147f380eca896c7343 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Wed, 4 Jun 2025 13:51:03 +0530 Subject: [PATCH 09/15] [Automated] Update the native jar versions --- ballerina/Ballerina.toml | 6 +++--- ballerina/Dependencies.toml | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index c9875e2..c96c35c 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "mcp" -version = "0.1.0" +version = "0.4.0" 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 = "0.1.0" -path = "../native/build/libs/mcp-native-0.1.0-SNAPSHOT.jar" +version = "0.4.0" +path = "../native/build/libs/mcp-native-0.4.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index e1c1367..3a983e4 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -108,9 +108,6 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.value"} ] -modules = [ - {org = "ballerina", packageName = "io", moduleName = "io"} -] [[package]] org = "ballerina" @@ -223,10 +220,9 @@ dependencies = [ [[package]] org = "ballerina" name = "mcp" -version = "0.1.0" +version = "0.4.0" dependencies = [ {org = "ballerina", name = "http"}, - {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"} ] modules = [ From 214c2051a85539f095087d8fff268449e331ed29 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Wed, 4 Jun 2025 13:51:42 +0530 Subject: [PATCH 10/15] Bump the mcp version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fec109c..a0aecee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=0.1.0-SNAPSHOT +version=0.4.0-SNAPSHOT ballerinaLangVersion=2201.12.0 ballerinaGradlePluginVersion=2.3.0 From ac59715dbc6c3972b5069c9b468e6862e6bc575b Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Wed, 4 Jun 2025 15:13:01 +0530 Subject: [PATCH 11/15] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 3a983e4..49b11d0 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -108,6 +108,9 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.value"} ] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] [[package]] org = "ballerina" @@ -223,6 +226,7 @@ name = "mcp" version = "0.4.0" dependencies = [ {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"} ] modules = [ From 8e75dccc90e219a2be7bf5dd1b6ea01e81956e89 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Wed, 4 Jun 2025 15:32:24 +0530 Subject: [PATCH 12/15] Address review comments --- ballerina/client.bal | 110 +++++++++--------- .../json_rpc_message_stream_transformer.bal | 17 ++- ballerina/streamable_http.bal | 2 +- ballerina/utils.bal | 11 +- 4 files changed, 74 insertions(+), 66 deletions(-) diff --git a/ballerina/client.bal b/ballerina/client.bal index 6b78916..1048d78 100644 --- a/ballerina/client.bal +++ b/ballerina/client.bal @@ -23,7 +23,7 @@ public type ClientConfiguration record {| |}; # Represents an MCP client built on top of the Streamable HTTP transport. -public distinct client class Client { +public distinct isolated client class Client { # MCP server URL. private final string serverUrl; # Client implementation details (e.g., name and version). @@ -47,8 +47,8 @@ public distinct client class Client { # + config - Optional configuration containing client capabilities. public isolated function init(string serverUrl, Implementation clientInfo, ClientConfiguration? config = ()) { self.serverUrl = serverUrl; - self.clientInfo = clientInfo; - self.clientCapabilities = config?.capabilities ?: {}; + self.clientInfo = clientInfo.cloneReadOnly(); + self.clientCapabilities = config?.capabilities.cloneReadOnly() ?: {}; } # Establishes a connection to the MCP server and performs protocol initialization. @@ -97,8 +97,6 @@ public distinct client class Client { method: "notifications/initialized" }; check self.sendNotificationMessage(initNotification); - - return; } else { return error ClientInitializationError( string `Initialization failed: unexpected response type '${(typeof response).toString()}' received from server.` @@ -111,13 +109,15 @@ public distinct client class Client { # # + return - Stream of JsonRpcMessages or a ClientError. isolated remote function subscribeToServerMessages() returns stream|ClientError { - StreamableHttpClientTransport? currentTransport = self.transport; - if currentTransport is () { - return error UninitializedTransportError( - "Subscription failed: client transport is not initialized. Call initialize() first." - ); + lock { + StreamableHttpClientTransport? currentTransport = self.transport; + if currentTransport is () { + return error UninitializedTransportError( + "Subscription failed: client transport is not initialized. Call initialize() first." + ); + } + return currentTransport.establishEventStream(); } - return check currentTransport.establishEventStream(); } # Retrieves the list of available tools from the server. @@ -162,23 +162,25 @@ public distinct client class Client { # # + return - A ClientError if closure fails, or nil on success. isolated remote function close() returns ClientError? { - StreamableHttpClientTransport? currentTransport = self.transport; - if currentTransport is () { - return error UninitializedTransportError( - "Closure failed: client transport is not initialized. Call initialize() first." - ); - } + lock { + StreamableHttpClientTransport? currentTransport = self.transport; + if currentTransport is () { + return error UninitializedTransportError( + "Closure failed: client transport is not initialized. Call initialize() first." + ); + } - do { - check currentTransport.terminateSession(); - lock { - self.transport = (); - self.serverCapabilities = (); - self.serverInfo = (); + do { + check currentTransport.terminateSession(); + lock { + self.transport = (); + self.serverCapabilities = (); + self.serverInfo = (); + } + return; + } on fail error e { + return error ClientError(string `Failed to disconnect from server: ${e.message()}`, e); } - return; - } on fail error e { - return error ClientError(string `Failed to disconnect from server: ${e.message()}`, e); } } @@ -187,25 +189,27 @@ public distinct client class Client { # + request - The request object to send. # + return - ServerResult, a stream of results, or a ClientError. private isolated function sendRequestMessage(Request request) returns ServerResult|ClientError { - StreamableHttpClientTransport? currentTransport = self.transport; - if currentTransport is () { - return error UninitializedTransportError( - "Cannot send request: client transport is not initialized. Call initialize() first." - ); - } - lock { - self.requestId += 1; + StreamableHttpClientTransport? currentTransport = self.transport; + if currentTransport is () { + return error UninitializedTransportError( + "Cannot send request: client transport is not initialized. Call initialize() first." + ); + } - JsonRpcRequest jsonRpcRequest = { - ...request, - jsonrpc: JSONRPC_VERSION, - id: self.requestId - }; + lock { + self.requestId += 1; + + JsonRpcRequest jsonRpcRequest = { + ...request.cloneReadOnly(), + jsonrpc: JSONRPC_VERSION, + id: self.requestId + }; - JsonRpcMessage|stream|StreamableHttpTransportError? response = - currentTransport.sendMessage(jsonRpcRequest); - return processServerResponse(response); + JsonRpcMessage|stream|StreamableHttpTransportError? response = + currentTransport.sendMessage(jsonRpcRequest); + return processServerResponse(response).cloneReadOnly(); + } } } @@ -214,18 +218,20 @@ public distinct client class Client { # + notification - The notification object to send. # + return - A ClientError if sending fails, or nil on success. private isolated function sendNotificationMessage(Notification notification) returns ClientError? { - StreamableHttpClientTransport? currentTransport = self.transport; - if currentTransport is () { - return error UninitializedTransportError( + lock { + StreamableHttpClientTransport? currentTransport = self.transport; + if currentTransport is () { + return error UninitializedTransportError( "Cannot send notification: client transport is not initialized. Call initialize() first." - ); - } + ); + } - JsonRpcNotification jsonRpcNotification = { - ...notification, - jsonrpc: JSONRPC_VERSION - }; + JsonRpcNotification jsonRpcNotification = { + ...notification.cloneReadOnly(), + jsonrpc: JSONRPC_VERSION + }; - _ = check currentTransport.sendMessage(jsonRpcNotification); + _ = check currentTransport.sendMessage(jsonRpcNotification); + } } } diff --git a/ballerina/json_rpc_message_stream_transformer.bal b/ballerina/json_rpc_message_stream_transformer.bal index 318effd..2a30d2d 100644 --- a/ballerina/json_rpc_message_stream_transformer.bal +++ b/ballerina/json_rpc_message_stream_transformer.bal @@ -96,17 +96,16 @@ isolated class JsonRpcMessageStreamTransformer { return error MissingSseDataError("SSE event is missing the required 'data' field."); } - json jsonData; - do { - jsonData = check eventData.fromJsonString(); - } on fail error e { - return error JsonParsingError(string `Failed to parse SSE event data as JSON: ${e.message()}`); + json|error jsonData = eventData.fromJsonString(); + if jsonData is error { + return error JsonParsingError(string `Failed to parse SSE event data as JSON: ${jsonData.message()}`); } - do { - return check jsonData.cloneWithType(); - } on fail error e { - return error TypeConversionError(string `Failed to convert JSON data to JsonRpcMessage: ${e.message()}`); + JsonRpcMessage|error message = jsonData.cloneWithType(JsonRpcMessage); + if message is error { + return error TypeConversionError(string `Failed to convert JSON data to JsonRpcMessage: ${message.message()}`); } + + return message; } } diff --git a/ballerina/streamable_http.bal b/ballerina/streamable_http.bal index 81bf4ff..7f3e8e4 100644 --- a/ballerina/streamable_http.bal +++ b/ballerina/streamable_http.bal @@ -54,7 +54,7 @@ isolated class StreamableHttpClientTransport { headers[ACCEPT_HEADER] = string `${CONTENT_TYPE_JSON}, ${CONTENT_TYPE_SSE}`; do { - http:Response response = check self.httpClient->post("/", message, headers = headers); + http:Response response = check self.httpClient->/.post(message, headers = headers); // Handle session ID in the initialization response. string|error sessionIdHeader = response.getHeader(SESSION_ID_HEADER); diff --git a/ballerina/utils.bal b/ballerina/utils.bal index 2b03502..192dd62 100644 --- a/ballerina/utils.bal +++ b/ballerina/utils.bal @@ -28,7 +28,7 @@ isolated function processServerResponse(JsonRpcMessage|stream Date: Thu, 5 Jun 2025 15:43:51 +0530 Subject: [PATCH 13/15] Address review comments --- .../stdlib/mcp/SseEventStreamHelper.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java b/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java index baa407e..8f26936 100644 --- a/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java +++ b/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java @@ -19,8 +19,12 @@ package io.ballerina.stdlib.mcp; import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.creators.ErrorCreator; import io.ballerina.runtime.api.values.BObject; import io.ballerina.runtime.api.values.BStream; +import io.ballerina.runtime.api.values.BString; + +import static io.ballerina.runtime.api.utils.StringUtils.fromString; /** * Utility class for handling Server-Sent Events (SSE) streams in Ballerina via Java interop. @@ -58,13 +62,14 @@ public static void attachSseStream(BObject object, BStream sseStream) { * * @param env The Ballerina runtime environment. * @param object The Ballerina object holding the SSE stream as native data. - * @return The next SSE event record, or null if the stream is exhausted. - * @throws IllegalStateException if the SSE stream has not been attached to the object. + * @return The next SSE event record, null if the stream is exhausted, + * or a Ballerina error object if unavailable. */ public static Object getNextSseEvent(Environment env, BObject object) { BStream sseStream = (BStream) object.getNativeData(SSE_STREAM_NATIVE_KEY); if (sseStream == null) { - throw new IllegalStateException("SSE stream not attached to the provided Ballerina object."); + BString errorMessage = fromString("SSE stream not attached to the provided Ballerina object."); + return ErrorCreator.createError(errorMessage); } BObject iteratorObject = sseStream.getIteratorObj(); // Use the Ballerina runtime to call the "next" method on the iterator and fetch the next event. @@ -78,13 +83,13 @@ public static Object getNextSseEvent(Environment env, BObject object) { * * @param env The Ballerina runtime environment. * @param object The Ballerina object holding the SSE stream as native data. - * @return The result of the close operation (could be null or error). - * @throws IllegalStateException if the SSE stream has not been attached to the object. + * @return The result of the close operation (could be null or a Ballerina error object). */ public static Object closeSseEventStream(Environment env, BObject object) { BStream sseStream = (BStream) object.getNativeData(SSE_STREAM_NATIVE_KEY); if (sseStream == null) { - throw new IllegalStateException("SSE stream not attached to the provided Ballerina object."); + BString errorMessage = fromString("SSE stream not attached to the provided Ballerina object."); + return ErrorCreator.createError(errorMessage); } BObject iteratorObject = sseStream.getIteratorObj(); // Use the Ballerina runtime to call the "close" method on the iterator and release resources. From fbc64f32e860d9ba8312be28872e746fec6b165f Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Fri, 6 Jun 2025 10:41:35 +0530 Subject: [PATCH 14/15] Address review comments --- ballerina/utils.bal | 12 +--- codecov.yml | 2 +- gradle.properties | 2 +- issue_template.md | 18 ------ .../stdlib/mcp/SseEventStreamHelper.java | 4 +- pull_request_template.md | 58 ++++--------------- 6 files changed, 17 insertions(+), 79 deletions(-) delete mode 100644 issue_template.md diff --git a/ballerina/utils.bal b/ballerina/utils.bal index 192dd62..a25a94d 100644 --- a/ballerina/utils.bal +++ b/ballerina/utils.bal @@ -21,27 +21,21 @@ isolated function processServerResponse(JsonRpcMessage|stream|StreamableHttpTransportError? serverResponse) returns ServerResult|ServerResponseError|StreamError { - // If response is a stream, extract the result from the stream. if serverResponse is stream { return extractResultFromMessageStream(serverResponse); } - // If response is a direct JsonRpcMessage, convert it to a result. if serverResponse is JsonRpcMessage { return extractResultFromMessage(serverResponse); } - // Null response: indicates malformed or missing server reply. if serverResponse is () { return error MalformedResponseError("Received null response from server."); } - // If a transport error is returned, wrap as a ServerResponseError. - if serverResponse is StreamableHttpTransportError { - return error ServerResponseError( - string `Transport error connecting to server: ${serverResponse.message()}` - ); - } + return error ServerResponseError( + string `Transport error connecting to server: ${serverResponse.message()}` + ); } # Extracts the first valid result from a stream of JsonRpcMessages. diff --git a/codecov.yml b/codecov.yml index b6e25d8..e570120 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,5 @@ fixes: - - "io/ballerina/lib/ai/plugin/::./compiler-plugin/src/main/java/io/ballerina/lib/ai/plugin/" + - "io/ballerina/stdlib/mcp/plugin/" coverage: precision: 2 diff --git a/gradle.properties b/gradle.properties index a0aecee..5494bd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ group=io.ballerina.stdlib version=0.4.0-SNAPSHOT ballerinaLangVersion=2201.12.0 -ballerinaGradlePluginVersion=2.3.0 +ballerinaGradlePluginVersion=3.0.0 checkstylePluginVersion=10.12.0 spotbugsPluginVersion=6.0.18 diff --git a/issue_template.md b/issue_template.md deleted file mode 100644 index 52a7563..0000000 --- a/issue_template.md +++ /dev/null @@ -1,18 +0,0 @@ -**Description:** - - -**Suggested Labels:** - - -**Suggested Assignees:** - - -**Affected Product Version:** - -**OS, DB, other environment details and versions:** - -**Steps to reproduce:** - - -**Related Issues:** - diff --git a/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java b/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java index 8f26936..b733df1 100644 --- a/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java +++ b/native/src/main/java/io/ballerina/stdlib/mcp/SseEventStreamHelper.java @@ -68,7 +68,7 @@ public static void attachSseStream(BObject object, BStream sseStream) { public static Object getNextSseEvent(Environment env, BObject object) { BStream sseStream = (BStream) object.getNativeData(SSE_STREAM_NATIVE_KEY); if (sseStream == null) { - BString errorMessage = fromString("SSE stream not attached to the provided Ballerina object."); + BString errorMessage = fromString("Unable to obtain elements from stream. SSE stream not found."); return ErrorCreator.createError(errorMessage); } BObject iteratorObject = sseStream.getIteratorObj(); @@ -88,7 +88,7 @@ public static Object getNextSseEvent(Environment env, BObject object) { public static Object closeSseEventStream(Environment env, BObject object) { BStream sseStream = (BStream) object.getNativeData(SSE_STREAM_NATIVE_KEY); if (sseStream == null) { - BString errorMessage = fromString("SSE stream not attached to the provided Ballerina object."); + BString errorMessage = fromString("Unable to obtain elements from stream. SSE stream not found."); return ErrorCreator.createError(errorMessage); } BObject iteratorObject = sseStream.getIteratorObj(); diff --git a/pull_request_template.md b/pull_request_template.md index 45221b5..4ad062f 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,52 +1,14 @@ ## Purpose -> Describe the problems, issues, or needs driving this feature/fix and include links to related issues in the following format: Resolves issue1, issue2, etc. -## Goals -> Describe the solutions that this feature/fix will introduce to resolve the problems described above +Fixes: -## Approach -> Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI (email documentation@wso2.com to review all UI text). Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here. +## Examples -## User stories -> Summary of user stories addressed by this change> - -## Release note -> Brief description of the new feature or bug fix as it will appear in the release notes - -## Documentation -> Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter “N/A” plus brief explanation of why there’s no doc impact - -## Training -> Link to the PR for changes to the training content in https://github.com/wso2/WSO2-Training, if applicable - -## Certification -> Type “Sent” when you have provided new/updated certification questions, plus four answers for each question (correct answer highlighted in bold), based on this change. Certification questions/answers should be sent to certification@wso2.com and NOT pasted in this PR. If there is no impact on certification exams, type “N/A” and explain why. - -## Marketing -> Link to drafts of marketing content that will describe and promote this feature, including product page changes, technical articles, blog posts, videos, etc., if applicable - -## Automation tests - - Unit tests - > Code coverage information - - Integration tests - > Details about the test cases and coverage - -## Security checks - - Followed secure coding standards in http://wso2.com/technical-reports/wso2-secure-engineering-guidelines? yes/no - - Ran FindSecurityBugs plugin and verified report? yes/no - - Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets? yes/no - -## Samples -> Provide high-level details about the samples related to this feature - -## Related PRs -> List any other related PRs - -## Migrations (if applicable) -> Describe migration steps and platforms on which migration has been tested - -## Test environment -> List all JDK versions, operating systems, databases, and browser/versions on which this feature/fix was tested - -## Learning -> Describe the research phase and any blog posts, patterns, libraries, or add-ons you used to solve the problem. +## Checklist +- [ ] Linked to an issue +- [ ] Updated the changelog +- [ ] Added tests +- [ ] Updated the spec +- [ ] Checked native-image compatibility +- [ ] No `commons` package changes (if there are any, please update the GraphQL version in [GraphQL tools](https://github.com/ballerina-platform/graphql-tools) and [Ballerina dev tools](https://github.com/ballerina-platform/ballerina-dev-tools)) +- [ ] No `compiler` package changes (if there are any, please update the GraphQL version in [Ballerina dev tools](https://github.com/ballerina-platform/ballerina-dev-tools)) From 6f531d4234fbcbdb7bc3d95ca100d654575a15a1 Mon Sep 17 00:00:00 2001 From: Azeem Muzammil Date: Fri, 6 Jun 2025 10:50:29 +0530 Subject: [PATCH 15/15] Address review comments --- ballerina/types.bal | 284 +++++++++++++++++--------------------------- 1 file changed, 107 insertions(+), 177 deletions(-) diff --git a/ballerina/types.bal b/ballerina/types.bal index fb930ef..e53a57c 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -33,11 +33,10 @@ public type ProgressToken string|int; public type Cursor string; # Represents a generic request in the protocol -# -# + method - The method name for the request -# + params - Optional parameters for the request public type Request record {| + # The method name for the request string method; + # Optional parameters for the request record { record {| # If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). @@ -48,21 +47,19 @@ public type Request record {| |}; # Represents a notification. -# -# + method - The method name of the notification -# + params - Additional parameters for the notification public type Notification record {| + # The method name of the notification string method; + # Additional parameters for the notification record { record {} _meta?; } params?; |}; # Base result type with common fields. -# -# + _meta - This result property is reserved by the protocol to allow clients and servers -# to attach additional metadata to their responses. public type Result record { + # This result property is reserved by the protocol to allow clients and servers + # to attach additional metadata to their responses. record {} _meta?; }; @@ -70,31 +67,28 @@ public type Result record { public type RequestId string|int; # A request that expects a response. -# -# + jsonrpc - The JSON-RPC protocol version -# + id - Identifier established by the client that should be returned in the response public type JsonRpcRequest record { *Request; + # The JSON-RPC protocol version JSONRPC_VERSION jsonrpc; + # Identifier established by the client that should be returned in the response RequestId id; }; # A notification which does not expect a response. -# -# + jsonrpc - The JSON-RPC protocol version public type JsonRpcNotification record { *Notification; + # The JSON-RPC protocol version JSONRPC_VERSION jsonrpc; }; # A successful (non-error) response to a request. -# -# + jsonrpc - The JSON-RPC protocol version -# + id - Identifier of the request -# + result - The result of the request public type JsonRpcResponse record {| + # The JSON-RPC protocol version JSONRPC_VERSION jsonrpc; + # Identifier of the request RequestId id; + # The result of the request ServerResult result; |}; @@ -106,13 +100,12 @@ public const INVALID_PARAMS = -32602; public const INTERNAL_ERROR = -32603; # A response to a request that indicates an error occurred. -# -# + jsonrpc - The JSON-RPC protocol version -# + id - Identifier of the request -# + error - The error information public type JsonRpcError record { + # The JSON-RPC protocol version JSONRPC_VERSION jsonrpc; + # Identifier of the request RequestId id; + # The error information record { # The error type that occurred int code; @@ -127,22 +120,13 @@ public type JsonRpcError record { public type EmptyResult Result; # This notification can be sent by either side to indicate that it is cancelling a previously-issued request. -# -# The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification -# MAY arrive after the request has already finished. -# -# This notification indicates that the result will be unused, so any associated processing SHOULD cease. -# -# A client MUST NOT attempt to cancel its `initialize` request. -# -# + method - The method name for this notification -# + params - The parameters for the cancellation notification public type CancelledNotification record {| *Notification; + # The method name for this notification "notifications/cancelled" method; + # The parameters for the cancellation notification record {| # The ID of the request to cancel. - # # This MUST correspond to the ID of a request previously issued in the same direction. RequestId requestId; # An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. @@ -151,12 +135,11 @@ public type CancelledNotification record {| |}; # This request is sent from the client to the server when it first connects, asking it to begin initialization. -# -# + method - Method name for the request -# + params - Parameters for the initialize request type InitializeRequest record {| *Request; + # Method name for the request "initialize" method; + # Parameters for the initialize request record {| # The latest version of the Model Context Protocol that the client supports. # The client MAY decide to support older versions as well. @@ -169,70 +152,66 @@ type InitializeRequest record {| |}; # After receiving an initialize request from the client, the server sends this response. -# -# + protocolVersion - The version of the Model Context Protocol that the server wants to use. -# This may not match the version that the client requested. -# If the client cannot support this version, it MUST disconnect. -# + capabilities - The capabilities of the server. -# + serverInfo - Information about the server implementation -# + instructions - Instructions describing how to use the server and its features. -# This can be used by clients to improve the LLM's understanding of available tools, resources, etc. -# It can be thought of like a "hint" to the model. -# For example, this information MAY be added to the system prompt. public type InitializeResult record {| *Result; + # The version of the Model Context Protocol that the server wants to use. + # This may not match the version that the client requested. + # If the client cannot support this version, it MUST disconnect. string protocolVersion; + # The capabilities of the server. ServerCapabilities capabilities; + # Information about the server implementation Implementation serverInfo; + # Instructions describing how to use the server and its features. + # This can be used by clients to improve the LLM's understanding of available tools, resources, etc. + # It can be thought of like a "hint" to the model. + # For example, this information MAY be added to the system prompt. string instructions?; |}; # This notification is sent from the client to the server after initialization has finished. -# -# + method - The method identifier for the notification, must be "notifications/initialized" public type InitializedNotification record {| *Notification; + # The method identifier for the notification, must be "notifications/initialized" "notifications/initialized" method; |}; # Capabilities a client may support. Known capabilities are defined here, in this schema, # but this is not a closed set: any client can define its own, additional capabilities. -# -# + experimental - Experimental, non-standard capabilities that the client supports. -# + roots - Present if the client supports listing roots. -# + sampling - Present if the client supports sampling from an LLM. public type ClientCapabilities record { + # Experimental, non-standard capabilities that the client supports. record {|record {}...;|} experimental?; + # Present if the client supports listing roots. record {| # Whether the client supports notifications for changes to the roots list. boolean listChanged?; |} roots?; + # Present if the client supports sampling from an LLM. record {} sampling?; }; # Capabilities that a server may support. Known capabilities are defined here, in this schema, # but this is not a closed set: any server can define its own, additional capabilities. -# -# + experimental - Experimental, non-standard capabilities that the server supports. -# + logging - Present if the server supports sending log messages to the client. -# + completions - Present if the server supports argument autocompletion suggestions. -# + prompts - Present if the server offers any prompt templates. -# + resources - Present if the server offers any resources to read. -# + tools - Present if the server offers any tools to call. public type ServerCapabilities record { + # Experimental, non-standard capabilities that the server supports. record {|record {}...;|} experimental?; + # Present if the server supports sending log messages to the client. record {} logging?; + # Present if the server supports argument autocompletion suggestions. record {} completions?; + # Present if the server offers any prompt templates. record {| # Whether this server supports notifications for changes to the prompt list. boolean listChanged?; |} prompts?; + # Present if the server offers any resources to read. record {| # Whether this server supports subscribing to resource updates. boolean subscribe?; # Whether this server supports notifications for changes to the resource list. boolean listChanged?; |} resources?; + # Present if the server offers any tools to call. record {| # Whether this server supports notifications for changes to the tool list. boolean listChanged?; @@ -240,31 +219,28 @@ public type ServerCapabilities record { }; # Describes the name and version of an MCP implementation. -# -# + name - The name of the implementation -# + version - The version of the implementation public type Implementation record {| + # The name of the implementation string name; + # The version of the implementation string version; |}; # A ping, issued by either the server or the client, to check that # the other party is still alive. The receiver must promptly respond, # or else may be disconnected. -# -# + method - The method name public type PingRequest record {| *Request; + # The method name "ping" method; |}; # An out-of-band notification used to inform the receiver of a progress update for a long-running request. -# -# + method - The method name for the notification -# + params - The parameters for the progress notification public type ProgressNotification record {| *Notification; + # The method name for the notification "notifications/progress" method; + # The parameters for the progress notification record { # The progress token which was given in the initial request, # used to associate this notification with the request that is proceeding. @@ -281,9 +257,8 @@ public type ProgressNotification record {| |}; # Represents a paginated request with optional cursor-based pagination. -# -# + params - Optional pagination parameters public type PaginatedRequest record {| + # Optional pagination parameters record {| # An opaque token representing the current pagination position. # If provided, the server should return results starting after this cursor. @@ -292,29 +267,24 @@ public type PaginatedRequest record {| |}; # Result that supports pagination -# -# + nextCursor - An opaque token representing the pagination position after the last returned result. -# If present, there may be more results available. public type PaginatedResult record {| *Result; + # An opaque token representing the pagination position after the last returned result. + # If present, there may be more results available. Cursor nextCursor?; |}; # An optional notification from the server to the client, informing it that the list of resources it can read from has changed. -# This may be issued by servers without any previous subscription from the client. -# -# + method - The JSON-RPC method name for resource list changed notifications public type ResourceListChangedNotification record {| *Notification; + # The JSON-RPC method name for resource list changed notifications "notifications/resources/list_changed" method; |}; # A notification from the server to the client, informing it that a resource has changed and may need to be read again. -# This should only be sent if the client previously sent a resources/subscribe request. -# -# + method - The JSON-RPC method name for resource updated notifications public type ResourceUpdatedNotification record {| *Notification; + # The JSON-RPC method name for resource updated notifications "notifications/resources/updated" method; # The parameters for the resource updated notification record { @@ -326,27 +296,24 @@ public type ResourceUpdatedNotification record {| |}; # The contents of a specific resource or sub-resource. -# -# + uri - The URI of this resource. -# + mimeType - The MIME type of this resource, if known. public type ResourceContents record {| + # The URI of this resource. string uri; + # The MIME type of this resource, if known. string mimeType?; |}; # Text resource contents -# -# + text - The text of the item. This must only be set if the item can actually be represented as text (not binary data). public type TextResourceContents record {| *ResourceContents; + # The text of the item. This must only be set if the item can actually be represented as text (not binary data). string text; |}; # Binary resource contents -# -# + blob - A base64-encoded string representing the binary data of the item. public type BlobResourceContents record {| *ResourceContents; + # A base64-encoded string representing the binary data of the item. string blob; |}; @@ -354,148 +321,120 @@ public type BlobResourceContents record {| public type Role "user"|"assistant"; # The contents of a resource, embedded into a prompt or tool call result. -# -# It is up to the client how best to render embedded resources for the benefit -# of the LLM and/or the user. -# -# + type - The type of content -# + resource - The resource content -# + annotations - Optional annotations for the client public type EmbeddedResource record {| + # The type of content "resource" 'type; + # The resource content TextResourceContents|BlobResourceContents 'resource; + # Optional annotations for the client Annotations annotations?; |}; # An optional notification from the server to the client, informing it that -# the list of prompts it offers has changed. This may be issued by servers -# without any previous subscription from the client. -# -# + method - The JSON-RPC method name for prompt list changed notifications +# the list of prompts it offers has changed. public type PromptListChangedNotification record {| *Notification; + # The JSON-RPC method name for prompt list changed notifications "notifications/prompts/list_changed" method; |}; # Sent from the client to request a list of tools the server has. -# -# + method - The method identifier for this request public type ListToolsRequest record {| *PaginatedRequest; + # The method identifier for this request "tools/list" method; |}; # The server's response to a tools/list request from the client. -# -# + tools - A list of tools available on the server. public type ListToolsResult record {| *PaginatedResult; + # A list of tools available on the server. Tool[] tools; |}; # The server's response to a tool call. -# -# Any errors that originate from the tool SHOULD be reported inside the result -# object, with `isError` set to true, _not_ as an MCP protocol-level error -# response. Otherwise, the LLM would not be able to see that an error occurred -# and self-correct. -# -# However, any errors in _finding_ the tool, an error indicating that the -# server does not support tool calls, or any other exceptional conditions, -# should be reported as an MCP error response. -# -# + content - The content of the tool call result -# + isError - Whether the tool call ended in an error. -# If not set, this is assumed to be false (the call was successful). public type CallToolResult record {| + # The content of the tool call result (TextContent|ImageContent|AudioContent|EmbeddedResource)[] content; + # Whether the tool call ended in an error. + # If not set, this is assumed to be false (the call was successful). boolean isError?; |}; # Used by the client to invoke a tool provided by the server. -# -# + method - The JSON-RPC method name -# + params - The parameters for the tool call public type CallToolRequest record {| + # The JSON-RPC method name "tools/call" method; + # The parameters for the tool call CallToolParams params; |}; # Parameters for the tools/call request -# -# + name - The name of the tool to invoke -# + arguments - Optional arguments to pass to the tool public type CallToolParams record {| + # The name of the tool to invoke string name; + # Optional arguments to pass to the tool record {} arguments?; |}; # An optional notification from the server to the client, informing it that the list of tools -# it offers has changed. This may be issued by servers without any previous subscription from the client. -# -# + method - The JSON-RPC method name for tool list changed notifications +# it offers has changed. public type ToolListChangedNotification record {| *Notification; + # The JSON-RPC method name for tool list changed notifications "notifications/tools/list_changed" method; |}; # Additional properties describing a Tool to clients. # NOTE: all properties in ToolAnnotations are **hints**. -# They are not guaranteed to provide a faithful description of -# tool behavior (including descriptive properties like `title`). -# Clients should never make tool use decisions based on ToolAnnotations -# received from untrusted servers. -# -# + title - A human-readable title for the tool. -# + readOnlyHint - If true, the tool does not modify its environment. -# Default: false -# + destructiveHint - If true, the tool may perform destructive updates to its environment. -# If false, the tool performs only additive updates. -# (This property is meaningful only when `readOnlyHint == false`) -# Default: true -# + idempotentHint - If true, calling the tool repeatedly with the same arguments -# will have no additional effect on the its environment. -# (This property is meaningful only when `readOnlyHint == false`) -# Default: false -# + openWorldHint - If true, this tool may interact with an "open world" of external -# entities. If false, the tool's domain of interaction is closed. -# For example, the world of a web search tool is open, whereas that -# of a memory tool is not. -# Default: true public type ToolAnnotations record {| + # A human-readable title for the tool. string title?; + # If true, the tool does not modify its environment. + # Default: false boolean readOnlyHint?; + # If true, the tool may perform destructive updates to its environment. + # If false, the tool performs only additive updates. + # (This property is meaningful only when `readOnlyHint == false`) + # Default: true boolean destructiveHint?; + # If true, calling the tool repeatedly with the same arguments + # will have no additional effect on the its environment. + # (This property is meaningful only when `readOnlyHint == false`) + # Default: false boolean idempotentHint?; + # If true, this tool may interact with an "open world" of external + # entities. If false, the tool's domain of interaction is closed. + # For example, the world of a web search tool is open, whereas that + # of a memory tool is not. + # Default: true boolean openWorldHint?; |}; # Definition for a tool the client can call. -# -# + name - The name of the tool -# + description - A human-readable description of the tool -# This can be used by clients to improve the LLM's understanding of available tools. -# + inputSchema - A JSON Schema object defining the expected parameters for the tool. -# + annotations - Optional additional tool information. public type Tool record {| + # The name of the tool string name; + # A human-readable description of the tool + # This can be used by clients to improve the LLM's understanding of available tools. string description?; + # A JSON Schema object defining the expected parameters for the tool. record { "object" 'type; record {|record {}...;|} properties?; string[] required?; } inputSchema; + # Optional additional tool information. ToolAnnotations annotations?; |}; # Notification of a log message passed from server to client. If no logging/setLevel request has been # sent from the client, the server MAY decide which messages to send automatically. -# -# + method - The method name for the notification -# + params - The parameters for the logging message notification public type LoggingMessageNotification record {| *Notification; + # The method name for the notification "notifications/message" method; + # The parameters for the logging message notification record { # The severity of this log message. LoggingLevel level; @@ -508,61 +447,53 @@ public type LoggingMessageNotification record {| |}; # The severity of a log message. -# -# These map to syslog message severities, as specified in RFC-5424: -# https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 public type LoggingLevel "debug"|"info"|"notice"|"warning"|"error"|"critical"|"alert"|"emergency"; # Optional annotations for the client. The client can use annotations to inform how objects are used or displayed -# -# + audience - Describes who the intended customer of this object or data is. -# This can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). -# + priority - Describes how important this data is for operating the server. -# A value of 1 means "most important," and indicates that the data is effectively required, -# while 0 means "least important," and indicates that the data is entirely optional. public type Annotations record {| + # Describes who the intended customer of this object or data is. + # This can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). Role[] audience?; + # Describes how important this data is for operating the server. + # A value of 1 means "most important," and indicates that the data is effectively required, + # while 0 means "least important," and indicates that the data is entirely optional. decimal priority?; |}; # Text provided to or from an LLM. -# -# + type - The type of content -# + text - The text content of the message -# + annotations - Optional annotations for the client public type TextContent record {| + # The type of content "text" 'type; + # The text content of the message string text; + # Optional annotations for the client Annotations annotations?; |}; # An image provided to or from an LLM. -# -# + type - The type of content -# + data - The base64-encoded image data -# + mimeType - The MIME type of the image. Different providers may support different image types. -# + annotations - Optional annotations for the client public type ImageContent record {| + # The type of content "image" 'type; + # The base64-encoded image data string data; + # The MIME type of the image. Different providers may support different image types. string mimeType; + # Optional annotations for the client Annotations annotations?; |}; # Audio provided to or from an LLM. -# -# + type - The type of content -# + data - The base64-encoded audio data -# + mimeType - The MIME type of the audio. Different providers may support different audio types. -# + annotations - Optional annotations for the client public type AudioContent record {| + # The type of content "audio" 'type; + # The base64-encoded audio data string data; + # The MIME type of the audio. Different providers may support different audio types. string mimeType; + # Optional annotations for the client Annotations annotations?; |}; -// Client messages # Represents a request sent from the client to the server. public type ClientRequest PingRequest|InitializeRequest|CallToolRequest|ListToolsRequest; @@ -572,7 +503,6 @@ public type ClientNotification CancelledNotification|ProgressNotification|Initia # Represents a result sent from the client to the server. public type ClientResult EmptyResult; -// Server messages # Represents a response sent from the server to the client. public type ServerRequest PingRequest;