158 lines
4.5 KiB
V
158 lines
4.5 KiB
V
module mcp
|
|
|
|
struct MockTransport {
|
|
mut:
|
|
incoming []string
|
|
sent []string
|
|
closed bool
|
|
}
|
|
|
|
fn (mut transport MockTransport) send(message string) ! {
|
|
transport.sent << message
|
|
}
|
|
|
|
fn (mut transport MockTransport) receive() !string {
|
|
if transport.incoming.len == 0 {
|
|
return error('no messages queued in MockTransport')
|
|
}
|
|
message := transport.incoming[0]
|
|
transport.incoming = if transport.incoming.len == 1 {
|
|
[]string{}
|
|
} else {
|
|
transport.incoming[1..].clone()
|
|
}
|
|
return message
|
|
}
|
|
|
|
fn (mut transport MockTransport) close() {
|
|
transport.closed = true
|
|
}
|
|
|
|
fn test_initialize_sends_the_mcp_handshake() {
|
|
mut transport := &MockTransport{
|
|
incoming: [
|
|
new_response(1, InitializeResult{
|
|
protocol_version: protocol_version
|
|
capabilities: '{"tools":{}}'
|
|
server_info: Implementation{
|
|
name: 'mock-server'
|
|
version: '1.0.0'
|
|
}
|
|
}, ResponseError{}).encode(),
|
|
]
|
|
}
|
|
mut client := new_client(transport, ClientConfig{
|
|
client_info: Implementation{
|
|
name: 'mcp-test-client'
|
|
version: '0.1.0'
|
|
}
|
|
capabilities: '{"roots":{"listChanged":true}}'
|
|
})
|
|
|
|
result := client.initialize()!
|
|
|
|
assert result.server_info.name == 'mock-server'
|
|
assert transport.sent.len == 2
|
|
|
|
request := decode_request(transport.sent[0])!
|
|
params := request.decode_params[InitializeParams]()!
|
|
assert request.method == 'initialize'
|
|
assert params.protocol_version == protocol_version
|
|
assert params.client_info.name == 'mcp-test-client'
|
|
assert params.capabilities == '{"roots":{"listChanged":true}}'
|
|
|
|
notification := decode_notification(transport.sent[1])!
|
|
assert notification.method == 'notifications/initialized'
|
|
assert notification.params == ''
|
|
}
|
|
|
|
fn test_request_buffers_server_messages_after_initialize() {
|
|
mut transport := &MockTransport{
|
|
incoming: [
|
|
new_response(1, InitializeResult{
|
|
protocol_version: protocol_version
|
|
capabilities: '{"tools":{}}'
|
|
server_info: Implementation{
|
|
name: 'mock-server'
|
|
version: '1.0.0'
|
|
}
|
|
}, ResponseError{}).encode(),
|
|
new_notification('notifications/tools/list_changed', empty).encode(),
|
|
new_request('server-1', 'roots/list', empty).encode(),
|
|
new_response(2, true, ResponseError{}).encode(),
|
|
]
|
|
}
|
|
mut client := new_client(transport, ClientConfig{})
|
|
client.initialize()!
|
|
|
|
response := client.request_message('ping', empty)!
|
|
|
|
assert response.result == 'true'
|
|
assert transport.sent.len == 3
|
|
assert decode_request(transport.sent[2])!.method == 'ping'
|
|
|
|
notifications := client.take_notifications()
|
|
assert notifications.len == 1
|
|
assert notifications[0].method == 'notifications/tools/list_changed'
|
|
|
|
requests := client.take_requests()
|
|
assert requests.len == 1
|
|
assert requests[0].method == 'roots/list'
|
|
assert requests[0].id == '"server-1"'
|
|
}
|
|
|
|
fn test_parse_sse_messages_reads_json_rpc_events() {
|
|
body := 'event: message\r\n' +
|
|
'data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":0.5}}\r\n' +
|
|
'\r\n' + 'event: message\r\n' + 'data: {"jsonrpc":"2.0","id":1,"result":true}\r\n' + '\r\n'
|
|
|
|
messages := parse_sse_messages(body)!
|
|
|
|
assert messages.len == 2
|
|
assert decode_notification(messages[0])!.method == 'notifications/progress'
|
|
assert decode_response(messages[1])!.result == 'true'
|
|
}
|
|
|
|
fn test_stdio_messages_handle_partial_reads() {
|
|
payload := new_notification('notifications/initialized', empty).encode()
|
|
frame := encode_stdio_message(payload)
|
|
assert frame.ends_with('\n')
|
|
mut buffer := frame[..frame.len - 1]
|
|
|
|
try_extract_stdio_message(buffer) or { assert err.msg() == NoFrameError{}.msg() }
|
|
|
|
buffer += frame[frame.len - 1..]
|
|
extracted := try_extract_stdio_message(buffer)!
|
|
buffer = extracted.remaining
|
|
message := extracted.message
|
|
|
|
assert buffer == ''
|
|
assert decode_notification(message)!.method == 'notifications/initialized'
|
|
}
|
|
|
|
fn test_stdio_messages_strip_embedded_newlines() {
|
|
payload := '{"jsonrpc":"2.0",\n"method":"ping"}'
|
|
frame := encode_stdio_message(payload)
|
|
assert frame == '{"jsonrpc":"2.0","method":"ping"}\n'
|
|
}
|
|
|
|
fn test_stdio_messages_skip_blank_lines() {
|
|
first := encode_stdio_message(new_notification('a', empty).encode())
|
|
second := encode_stdio_message(new_notification('b', empty).encode())
|
|
buffer := first + '\n\n' + second
|
|
|
|
frame_a := try_extract_stdio_message(buffer)!
|
|
assert decode_notification(frame_a.message)!.method == 'a'
|
|
frame_b := try_extract_stdio_message(frame_a.remaining)!
|
|
assert decode_notification(frame_b.message)!.method == 'b'
|
|
}
|
|
|
|
fn test_close_delegates_to_the_transport() {
|
|
mut transport := &MockTransport{}
|
|
mut client := new_client(transport, ClientConfig{})
|
|
|
|
client.close()
|
|
|
|
assert transport.closed
|
|
}
|