Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion example/client_stdio.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// filepath: /Users/jhin.lee/Documents/personal/mcp_example/mcp_dart/example/client_stdio.dart
import 'dart:async';
import 'dart:io';

Expand Down
30 changes: 26 additions & 4 deletions lib/src/shared/uuid.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import 'dart:math';

/// Generates a UUID (version 4).
/// Generates a RFC4122 compliant UUID (version 4).
///
/// A version 4 UUID is randomly generated. This implementation follows the
/// format specified in RFC4122 with the appropriate bits set to identify
/// it as a version 4, variant 1 UUID.
///
/// Returns a string representation of the UUID in the format:
/// 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
String generateUUID() {
// Constants for UUID version 4
const int uuidVersion = 0x40; // Version 4 (random)
const int uuidVariant = 0x80; // Variant 1 (RFC4122)

final random = Random.secure();
final bytes = List<int>.generate(16, (i) => random.nextInt(256));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;

// Set the version bits (bits 6-7 of 7th byte to 0b01)
bytes[6] = (bytes[6] & 0x0f) | uuidVersion;

// Set the variant bits (bits 6-7 of 9th byte to 0b10)
bytes[8] = (bytes[8] & 0x3f) | uuidVariant;

// Convert to hex and format with hyphens
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}';

return '${hex.substring(0, 8)}-'
'${hex.substring(8, 12)}-'
'${hex.substring(12, 16)}-'
'${hex.substring(16, 20)}-'
'${hex.substring(20)}';
}
134 changes: 134 additions & 0 deletions test/integration/stdio_integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';

import 'package:mcp_dart/mcp_dart.dart';
import 'package:test/test.dart';

void main() {
group('stdio transport integration test', () {
late Client client;
late StdioClientTransport transport;
final stderrOutput = <String>[];
StreamSubscription<String>? stderrSub;

// Path to the example stdio server file
final String serverFilePath =
'${Directory.current.path}/example/server_stdio.dart';

setUp(() async {
// Verify the server file exists
final serverFile = File(serverFilePath);
expect(await serverFile.exists(), isTrue,
reason: 'Example server file not found');

// Create the client and transport
client =
Client(Implementation(name: "test-stdio-client", version: "1.0.0"));
transport = StdioClientTransport(
StdioServerParameters(
command: 'dart',
args: [serverFilePath],
stderrMode: ProcessStartMode.normal, // Pipe stderr for debugging
),
);

// Set up error handlers
client.onerror = (error) => fail('Client error: $error');

transport.onerror = (error) {
// Don't fail here as some non-critical errors might occur
stderrOutput.add('Transport error: $error');
};

// Capture stderr output from the server process
stderrSub = transport.stderr
?.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
stderrOutput.add(line);
});
});

tearDown(() async {
// Clean up resources
try {
await transport.close();
} catch (e) {
// Ignore errors during cleanup
}

await stderrSub?.cancel();
});

test('client and server communicate successfully using stdio transport',
() async {
// Connect client to the transport and establish communication
await client.connect(transport);

// Make sure connection is established
await Future.delayed(Duration(milliseconds: 500));

// Get available tools
final tools = await client.listTools();
expect(tools.tools.isNotEmpty, isTrue,
reason: 'Server should return at least one tool');
expect(tools.tools.any((tool) => tool.name == 'calculate'), isTrue,
reason: 'Server should have a "calculate" tool');

// Test all calculator operations
final operations = [
{
'operation': 'add',
'a': 5,
'b': 3,
'expected': 'Result: 8',
'description': 'Addition'
},
{
'operation': 'subtract',
'a': 10,
'b': 4,
'expected': 'Result: 6',
'description': 'Subtraction'
},
{
'operation': 'multiply',
'a': 6,
'b': 7,
'expected': 'Result: 42',
'description': 'Multiplication'
},
{
'operation': 'divide',
'a': 20,
'b': 5,
'expected': r'Result: 4(\.0)?$',
'description': 'Division',
'isRegex': true
}
];

for (final op in operations) {
final params = CallToolRequestParams(
name: 'calculate',
arguments: {'operation': op['operation'], 'a': op['a'], 'b': op['b']},
);

final result = await client.callTool(params);
expect(result.content.first is TextContent, isTrue,
reason: 'Result should contain TextContent');

final textContent = result.content.first as TextContent;

if (op['isRegex'] == true) {
expect(textContent.text, matches(op['expected'] as String),
reason: '${op['description']} result incorrect');
} else {
expect(textContent.text, op['expected'],
reason: '${op['description']} result incorrect');
}
}
}, timeout: Timeout(Duration(seconds: 30)));
});
}
Loading