Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
39e8445
[Automated] Update the native jar versions
AzeemMuzammil Jul 15, 2025
2abc576
Implement initial version of MCP server
Jun 9, 2025
ad45fc3
[Automated] Update the native jar versions
Jun 24, 2025
ea3f627
Remove unnecessary code
Jun 24, 2025
91bc0af
[Automated] Update the native jar versions
Jun 24, 2025
2c9086e
Refactor dispatcher service
Jun 24, 2025
223172a
Fix mcp service names
Jun 26, 2025
27f499a
[Automated] Update the native jar versions
Jun 26, 2025
f12d383
Fix compilation error
Jun 26, 2025
877e7d1
Fix naming issue
Jun 26, 2025
42b72db
Fix function symbol nullity
Jun 26, 2025
5750949
Fix error codes
Jun 26, 2025
5aa1b26
Remove unnecessary records
Jun 30, 2025
98f5de5
Address review comments
Jun 30, 2025
7596adf
[Automated] Update the native jar versions
Jun 30, 2025
facbfcb
[Automated] Update the native jar versions
Jun 30, 2025
e2cfadf
Fix error with cloning type
Jun 30, 2025
565d2d0
[Automated] Update the native jar versions
Jul 1, 2025
a4e5854
Move SericeConfigs to annotations
Jul 1, 2025
de0239a
[Automated] Update the native jar versions
Jul 3, 2025
1cde6c3
Address review comments
Jul 3, 2025
bf8c9a1
Fix the multi session
Jul 3, 2025
35dfa08
Remove isolated from servie type
Jul 3, 2025
cf3cc63
Fix warnings
Jul 3, 2025
759a7a7
Move initialization into init
Jul 4, 2025
1489a08
Rename McpTool to ToolDefinition
Jul 4, 2025
50cd39c
Fix attach detach logic
Jul 4, 2025
54b3ed1
Address review comments
Jul 8, 2025
1040605
[Automated] Update the native jar versions
AzeemMuzammil Jul 15, 2025
690e3ed
Fix merge conflicts
AzeemMuzammil Jul 15, 2025
2fcf652
Address review comments
AzeemMuzammil Jul 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ target
# Ballerina
velocity.log*
*Ballerina.lock

# AI Tools
CLAUDE.md
9 changes: 9 additions & 0 deletions ballerina/CompilerPlugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[plugin]
id = "mcp-compiler-plugin"
class = "io.ballerina.stdlib.mcp.plugin.McpCompilerPlugin"

[[dependency]]
path = "../compiler-plugin/build/libs/mcp-compiler-plugin-0.4.3-SNAPSHOT.jar"

[[dependency]]
path = "../compiler-plugin/build/libs/ballerina-to-openapi-2.3.0.jar"
14 changes: 5 additions & 9 deletions ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

[ballerina]
dependencies-toml-version = "2"
distribution-version = "2201.12.7"
distribution-version = "2201.12.0"

[[package]]
org = "ballerina"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -219,19 +216,15 @@ dependencies = [
{org = "ballerina", name = "lang.value"},
{org = "ballerina", name = "observe"}
]
modules = [
{org = "ballerina", packageName = "log", moduleName = "log"}
]

[[package]]
org = "ballerina"
name = "mcp"
version = "0.4.3"
dependencies = [
{org = "ballerina", name = "http"},
{org = "ballerina", name = "io"},
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "log"}
{org = "ballerina", name = "uuid"}
]
modules = [
{org = "ballerina", packageName = "mcp", moduleName = "mcp"}
Expand Down Expand Up @@ -314,4 +307,7 @@ dependencies = [
{org = "ballerina", name = "lang.int"},
{org = "ballerina", name = "time"}
]
modules = [
{org = "ballerina", packageName = "uuid", moduleName = "uuid"}
]

9 changes: 9 additions & 0 deletions ballerina/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ def packageName = "mcp"
def packageOrg = "ballerina"
def tomlVersion = stripBallerinaExtensionVersion("${project.version}")
def ballerinaTomlFilePlaceHolder = new File("${project.rootDir}/build-config/resources/Ballerina.toml")
def compilerPluginTomlFilePlaceHolder = new File("${project.rootDir}/build-config/resources/CompilerPlugin.toml")
def ballerinaTomlFile = new File("$project.projectDir/Ballerina.toml")
def compilerPluginTomlFile = new File("$project.projectDir/CompilerPlugin.toml")

def stripBallerinaExtensionVersion(String extVersion) {
if (extVersion.matches(project.ext.timestampedVersionRegex)) {
Expand Down Expand Up @@ -55,6 +57,11 @@ task updateTomlFiles {
def newConfig = ballerinaTomlFilePlaceHolder.text.replace("@project.version@", project.version)
newConfig = newConfig.replace("@toml.version@", tomlVersion)
ballerinaTomlFile.text = newConfig

def ballerinaToOpenApiVersion = project.ballerinaToOpenApiVersion
def newCompilerPluginToml = compilerPluginTomlFilePlaceHolder.text.replace("@project.version@", project.version)
newCompilerPluginToml = newCompilerPluginToml.replace("@ballerinaToOpenApiVersion.version@", ballerinaToOpenApiVersion)
compilerPluginTomlFile.text = newCompilerPluginToml
}
}

Expand Down Expand Up @@ -94,7 +101,9 @@ updateTomlFiles.dependsOn copyStdlibs

build.dependsOn "generatePomFileForMavenPublication"
build.dependsOn ":${packageName}-native:build"
build.dependsOn ":${packageName}-compiler-plugin:build"
test.dependsOn ":${packageName}-native:build"
test.dependsOn ":${packageName}-compiler-plugin:build"

publishToMavenLocal.dependsOn build
publish.dependsOn build
183 changes: 64 additions & 119 deletions ballerina/client.bal
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public type ClientConfiguration record {|
ClientCapabilityConfiguration capabilityConfig?;
|};

# Configuration options for initializing an MCP client.
# Configuration for MCP client capabilities.
public type ClientCapabilityConfiguration record {|
# Capabilities to be advertised by this client.
ClientCapabilities capabilities?;
Expand All @@ -33,112 +33,84 @@ public type ClientCapabilityConfiguration record {|

# Represents an MCP client built on top of the Streamable HTTP transport.
public distinct isolated client class Client {
# MCP server URL.
private final string serverUrl;
# Client implementation details (e.g., name and version).
private final Implementation clientInfo;
# Capabilities supported by the client.
private final ClientCapabilities clientCapabilities;

# Transport for communication with the MCP server.
private StreamableHttpClientTransport? transport = ();
private StreamableHttpClientTransport transport;
# Server capabilities.
private ServerCapabilities? serverCapabilities = ();
# 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.
# Initializes a new MCP client and establishes connection to the server.
# Performs protocol handshake and capability exchange. Client is ready for use after construction.
#
# + serverUrl - MCP server URL.
# + clientInfo - Client details, such as name and version.
# + config - Optional configuration containing client capabilities.
public isolated function init(string serverUrl, *ClientConfiguration config) {
self.serverUrl = serverUrl;
self.clientInfo = config.info.cloneReadOnly();
self.clientCapabilities = (config.capabilityConfig?.capabilities).cloneReadOnly() ?: {};
}

# 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 {
// Create and initialize transport.
StreamableHttpClientTransport newTransport = check new StreamableHttpClientTransport(self.serverUrl);
self.transport = newTransport;

string? sessionId = newTransport.getSessionId();
# + serverUrl - MCP server URL
# + config - Client configuration including info and capabilities
# + return - ClientError if initialization fails, nil on success
public isolated function init(string serverUrl, *ClientConfiguration config) returns ClientError? {
// Create and initialize transport.
StreamableHttpClientTransport newTransport = check new (serverUrl);
self.transport = newTransport;

string? sessionId = newTransport.getSessionId();

// If a session ID exists, assume reconnection and skip initialization.
if sessionId is string {
return;
}

// If a session ID exists, assume reconnection and skip initialization.
if sessionId is string {
return;
// Prepare and send the initialization request.
InitializeRequest initRequest = {
params: {
protocolVersion: LATEST_PROTOCOL_VERSION,
capabilities: config.capabilityConfig?.capabilities ?: {},
clientInfo: config.info
}
};

// Prepare and send the initialization request.
InitializeRequest initRequest = {
method: "initialize",
params: {
protocolVersion: LATEST_PROTOCOL_VERSION,
capabilities: self.clientCapabilities,
clientInfo: self.clientInfo
}
};
ServerResult response = check self.sendRequestMessage(initRequest);

ServerResult response = check self.sendRequestMessage(initRequest);

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()}.`
);
}

// 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.sendNotificationMessage(initNotification);
} else {
return error ClientInitializationError(
string `Initialization failed: unexpected response type '${
(typeof response).toString()}' received from server.`
);
}
if !(response is InitializeResult) {
return error ClientInitializationError(
string `Initialization failed: unexpected response type '${
(typeof response).toString()}' received from server.`
);
}

final 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()}.`
);
}

// Store server capabilities and info.
self.serverCapabilities = response.capabilities.cloneReadOnly();
self.serverInfo = response.serverInfo.cloneReadOnly();

// Send notification to complete initialization.
InitializedNotification initNotification = {};
check self.sendNotificationMessage(initNotification);
}

# 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<JsonRpcMessage, StreamError?>|ClientError {
lock {
StreamableHttpClientTransport? currentTransport = self.transport;
if currentTransport is () {
return error UninitializedTransportError(
"Subscription failed: client transport is not initialized."
);
}
return currentTransport.establishEventStream();
return self.transport.establishEventStream();
}
}

# 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"
};
ListToolsRequest listToolsRequest = {};

ServerResult result = check self.sendRequestMessage(listToolsRequest);
if result is ListToolsResult {
Expand All @@ -156,7 +128,6 @@ public distinct isolated client class Client {
# + 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
};

Expand All @@ -175,20 +146,10 @@ public distinct isolated client class Client {
# + return - A ClientError if closure fails, or nil on success.
isolated remote function close() returns ClientError? {
lock {
StreamableHttpClientTransport? currentTransport = self.transport;
if currentTransport is () {
return error UninitializedTransportError(
"Closure failed: client transport is not initialized."
);
}

do {
check currentTransport.terminateSession();
lock {
self.transport = ();
self.serverCapabilities = ();
self.serverInfo = ();
}
check self.transport.terminateSession();
self.serverCapabilities = ();
self.serverInfo = ();
return;
} on fail error e {
return error ClientError(string `Failed to disconnect from server: ${e.message()}`, e);
Expand All @@ -202,26 +163,17 @@ public distinct isolated client class Client {
# + return - ServerResult, a stream of results, or a ClientError.
private isolated function sendRequestMessage(Request request) returns ServerResult|ClientError {
lock {
StreamableHttpClientTransport? currentTransport = self.transport;
if currentTransport is () {
return error UninitializedTransportError(
"Cannot send request: client transport is not initialized."
);
}
self.requestId += 1;

lock {
self.requestId += 1;

JsonRpcRequest jsonRpcRequest = {
...request.cloneReadOnly(),
jsonrpc: JSONRPC_VERSION,
id: self.requestId
};
JsonRpcRequest jsonRpcRequest = {
...request.cloneReadOnly(),
jsonrpc: JSONRPC_VERSION,
id: self.requestId
};

JsonRpcMessage|stream<JsonRpcMessage, StreamError?>|StreamableHttpTransportError? response =
currentTransport.sendMessage(jsonRpcRequest);
return processServerResponse(response).cloneReadOnly();
}
JsonRpcMessage|stream<JsonRpcMessage, StreamError?>|StreamableHttpTransportError? response =
self.transport.sendMessage(jsonRpcRequest);
return processServerResponse(response).cloneReadOnly();
}
}

Expand All @@ -231,19 +183,12 @@ public distinct isolated client class Client {
# + return - A ClientError if sending fails, or nil on success.
private isolated function sendNotificationMessage(Notification notification) returns ClientError? {
lock {
StreamableHttpClientTransport? currentTransport = self.transport;
if currentTransport is () {
return error UninitializedTransportError(
"Cannot send notification: client transport is not initialized."
);
}

JsonRpcNotification jsonRpcNotification = {
...notification.cloneReadOnly(),
jsonrpc: JSONRPC_VERSION
};

_ = check currentTransport.sendMessage(jsonRpcNotification);
_ = check self.transport.sendMessage(jsonRpcNotification);
}
}
}
Loading