v / vlib / mcp / mcp_test.v
157 lines · 130 sloc · 4.52 KB · 7b55539b6e355cd5930ad6213fc49d3c884821dc
Raw
1module mcp
2
3struct MockTransport {
4mut:
5 incoming []string
6 sent []string
7 closed bool
8}
9
10fn (mut transport MockTransport) send(message string) ! {
11 transport.sent << message
12}
13
14fn (mut transport MockTransport) receive() !string {
15 if transport.incoming.len == 0 {
16 return error('no messages queued in MockTransport')
17 }
18 message := transport.incoming[0]
19 transport.incoming = if transport.incoming.len == 1 {
20 []string{}
21 } else {
22 transport.incoming[1..].clone()
23 }
24 return message
25}
26
27fn (mut transport MockTransport) close() {
28 transport.closed = true
29}
30
31fn test_initialize_sends_the_mcp_handshake() {
32 mut transport := &MockTransport{
33 incoming: [
34 new_response(1, InitializeResult{
35 protocol_version: protocol_version
36 capabilities: '{"tools":{}}'
37 server_info: Implementation{
38 name: 'mock-server'
39 version: '1.0.0'
40 }
41 }, ResponseError{}).encode(),
42 ]
43 }
44 mut client := new_client(transport, ClientConfig{
45 client_info: Implementation{
46 name: 'mcp-test-client'
47 version: '0.1.0'
48 }
49 capabilities: '{"roots":{"listChanged":true}}'
50 })
51
52 result := client.initialize()!
53
54 assert result.server_info.name == 'mock-server'
55 assert transport.sent.len == 2
56
57 request := decode_request(transport.sent[0])!
58 params := request.decode_params[InitializeParams]()!
59 assert request.method == 'initialize'
60 assert params.protocol_version == protocol_version
61 assert params.client_info.name == 'mcp-test-client'
62 assert params.capabilities == '{"roots":{"listChanged":true}}'
63
64 notification := decode_notification(transport.sent[1])!
65 assert notification.method == 'notifications/initialized'
66 assert notification.params == ''
67}
68
69fn test_request_buffers_server_messages_after_initialize() {
70 mut transport := &MockTransport{
71 incoming: [
72 new_response(1, InitializeResult{
73 protocol_version: protocol_version
74 capabilities: '{"tools":{}}'
75 server_info: Implementation{
76 name: 'mock-server'
77 version: '1.0.0'
78 }
79 }, ResponseError{}).encode(),
80 new_notification('notifications/tools/list_changed', empty).encode(),
81 new_request('server-1', 'roots/list', empty).encode(),
82 new_response(2, true, ResponseError{}).encode(),
83 ]
84 }
85 mut client := new_client(transport, ClientConfig{})
86 client.initialize()!
87
88 response := client.request_message('ping', empty)!
89
90 assert response.result == 'true'
91 assert transport.sent.len == 3
92 assert decode_request(transport.sent[2])!.method == 'ping'
93
94 notifications := client.take_notifications()
95 assert notifications.len == 1
96 assert notifications[0].method == 'notifications/tools/list_changed'
97
98 requests := client.take_requests()
99 assert requests.len == 1
100 assert requests[0].method == 'roots/list'
101 assert requests[0].id == '"server-1"'
102}
103
104fn test_parse_sse_messages_reads_json_rpc_events() {
105 body := 'event: message\r\n' +
106 'data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":0.5}}\r\n' +
107 '\r\n' + 'event: message\r\n' + 'data: {"jsonrpc":"2.0","id":1,"result":true}\r\n' + '\r\n'
108
109 messages := parse_sse_messages(body)!
110
111 assert messages.len == 2
112 assert decode_notification(messages[0])!.method == 'notifications/progress'
113 assert decode_response(messages[1])!.result == 'true'
114}
115
116fn test_stdio_messages_handle_partial_reads() {
117 payload := new_notification('notifications/initialized', empty).encode()
118 frame := encode_stdio_message(payload)
119 assert frame.ends_with('\n')
120 mut buffer := frame[..frame.len - 1]
121
122 try_extract_stdio_message(buffer) or { assert err.msg() == NoFrameError{}.msg() }
123
124 buffer += frame[frame.len - 1..]
125 extracted := try_extract_stdio_message(buffer)!
126 buffer = extracted.remaining
127 message := extracted.message
128
129 assert buffer == ''
130 assert decode_notification(message)!.method == 'notifications/initialized'
131}
132
133fn test_stdio_messages_strip_embedded_newlines() {
134 payload := '{"jsonrpc":"2.0",\n"method":"ping"}'
135 frame := encode_stdio_message(payload)
136 assert frame == '{"jsonrpc":"2.0","method":"ping"}\n'
137}
138
139fn test_stdio_messages_skip_blank_lines() {
140 first := encode_stdio_message(new_notification('a', empty).encode())
141 second := encode_stdio_message(new_notification('b', empty).encode())
142 buffer := first + '\n\n' + second
143
144 frame_a := try_extract_stdio_message(buffer)!
145 assert decode_notification(frame_a.message)!.method == 'a'
146 frame_b := try_extract_stdio_message(frame_a.remaining)!
147 assert decode_notification(frame_b.message)!.method == 'b'
148}
149
150fn test_close_delegates_to_the_transport() {
151 mut transport := &MockTransport{}
152 mut client := new_client(transport, ClientConfig{})
153
154 client.close()
155
156 assert transport.closed
157}
158