Skip to content

Commit ae626ae

Browse files
authored
Merge pull request #22 from leehack/add_testings
Add some testings
2 parents b63308e + 2806650 commit ae626ae

File tree

4 files changed

+543
-5
lines changed

4 files changed

+543
-5
lines changed

example/client_stdio.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// filepath: /Users/jhin.lee/Documents/personal/mcp_example/mcp_dart/example/client_stdio.dart
21
import 'dart:async';
32
import 'dart:io';
43

lib/src/shared/uuid.dart

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
import 'dart:math';
22

3-
/// Generates a UUID (version 4).
3+
/// Generates a RFC4122 compliant UUID (version 4).
4+
///
5+
/// A version 4 UUID is randomly generated. This implementation follows the
6+
/// format specified in RFC4122 with the appropriate bits set to identify
7+
/// it as a version 4, variant 1 UUID.
8+
///
9+
/// Returns a string representation of the UUID in the format:
10+
/// 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
411
String generateUUID() {
12+
// Constants for UUID version 4
13+
const int uuidVersion = 0x40; // Version 4 (random)
14+
const int uuidVariant = 0x80; // Variant 1 (RFC4122)
15+
516
final random = Random.secure();
617
final bytes = List<int>.generate(16, (i) => random.nextInt(256));
7-
bytes[6] = (bytes[6] & 0x0f) | 0x40;
8-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
18+
19+
// Set the version bits (bits 6-7 of 7th byte to 0b01)
20+
bytes[6] = (bytes[6] & 0x0f) | uuidVersion;
21+
22+
// Set the variant bits (bits 6-7 of 9th byte to 0b10)
23+
bytes[8] = (bytes[8] & 0x3f) | uuidVariant;
24+
25+
// Convert to hex and format with hyphens
926
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
10-
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}';
27+
28+
return '${hex.substring(0, 8)}-'
29+
'${hex.substring(8, 12)}-'
30+
'${hex.substring(12, 16)}-'
31+
'${hex.substring(16, 20)}-'
32+
'${hex.substring(20)}';
1133
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import 'dart:async';
2+
import 'dart:io';
3+
import 'dart:convert';
4+
5+
import 'package:mcp_dart/mcp_dart.dart';
6+
import 'package:test/test.dart';
7+
8+
void main() {
9+
group('stdio transport integration test', () {
10+
late Client client;
11+
late StdioClientTransport transport;
12+
final stderrOutput = <String>[];
13+
StreamSubscription<String>? stderrSub;
14+
15+
// Path to the example stdio server file
16+
final String serverFilePath =
17+
'${Directory.current.path}/example/server_stdio.dart';
18+
19+
setUp(() async {
20+
// Verify the server file exists
21+
final serverFile = File(serverFilePath);
22+
expect(await serverFile.exists(), isTrue,
23+
reason: 'Example server file not found');
24+
25+
// Create the client and transport
26+
client =
27+
Client(Implementation(name: "test-stdio-client", version: "1.0.0"));
28+
transport = StdioClientTransport(
29+
StdioServerParameters(
30+
command: 'dart',
31+
args: [serverFilePath],
32+
stderrMode: ProcessStartMode.normal, // Pipe stderr for debugging
33+
),
34+
);
35+
36+
// Set up error handlers
37+
client.onerror = (error) => fail('Client error: $error');
38+
39+
transport.onerror = (error) {
40+
// Don't fail here as some non-critical errors might occur
41+
stderrOutput.add('Transport error: $error');
42+
};
43+
44+
// Capture stderr output from the server process
45+
stderrSub = transport.stderr
46+
?.transform(utf8.decoder)
47+
.transform(const LineSplitter())
48+
.listen((line) {
49+
stderrOutput.add(line);
50+
});
51+
});
52+
53+
tearDown(() async {
54+
// Clean up resources
55+
try {
56+
await transport.close();
57+
} catch (e) {
58+
// Ignore errors during cleanup
59+
}
60+
61+
await stderrSub?.cancel();
62+
});
63+
64+
test('client and server communicate successfully using stdio transport',
65+
() async {
66+
// Connect client to the transport and establish communication
67+
await client.connect(transport);
68+
69+
// Make sure connection is established
70+
await Future.delayed(Duration(milliseconds: 500));
71+
72+
// Get available tools
73+
final tools = await client.listTools();
74+
expect(tools.tools.isNotEmpty, isTrue,
75+
reason: 'Server should return at least one tool');
76+
expect(tools.tools.any((tool) => tool.name == 'calculate'), isTrue,
77+
reason: 'Server should have a "calculate" tool');
78+
79+
// Test all calculator operations
80+
final operations = [
81+
{
82+
'operation': 'add',
83+
'a': 5,
84+
'b': 3,
85+
'expected': 'Result: 8',
86+
'description': 'Addition'
87+
},
88+
{
89+
'operation': 'subtract',
90+
'a': 10,
91+
'b': 4,
92+
'expected': 'Result: 6',
93+
'description': 'Subtraction'
94+
},
95+
{
96+
'operation': 'multiply',
97+
'a': 6,
98+
'b': 7,
99+
'expected': 'Result: 42',
100+
'description': 'Multiplication'
101+
},
102+
{
103+
'operation': 'divide',
104+
'a': 20,
105+
'b': 5,
106+
'expected': r'Result: 4(\.0)?$',
107+
'description': 'Division',
108+
'isRegex': true
109+
}
110+
];
111+
112+
for (final op in operations) {
113+
final params = CallToolRequestParams(
114+
name: 'calculate',
115+
arguments: {'operation': op['operation'], 'a': op['a'], 'b': op['b']},
116+
);
117+
118+
final result = await client.callTool(params);
119+
expect(result.content.first is TextContent, isTrue,
120+
reason: 'Result should contain TextContent');
121+
122+
final textContent = result.content.first as TextContent;
123+
124+
if (op['isRegex'] == true) {
125+
expect(textContent.text, matches(op['expected'] as String),
126+
reason: '${op['description']} result incorrect');
127+
} else {
128+
expect(textContent.text, op['expected'],
129+
reason: '${op['description']} result incorrect');
130+
}
131+
}
132+
}, timeout: Timeout(Duration(seconds: 30)));
133+
});
134+
}

0 commit comments

Comments
 (0)