module mcp import net.http import time fn test_server_routes_initialize_and_registered_features() { mut server := new_server( name: 'test-server' version: '1.2.3' instructions: 'Be precise.' ) server.add_tool(Tool{ name: 'say_hello' description: 'Returns a greeting' }, fn (ctx Context, arguments string) !ToolResult { assert ctx.session_id == stdio_session_id assert ctx.transport == .stdio assert arguments == '{"name":"V"}' return tool_text_result('Hello, V!') })! server.add_resource(Resource{ uri: 'resource://guide' name: 'guide' mime_type: 'text/plain' }, fn (_ Context, uri string) !ReadResourceResult { return ReadResourceResult{ contents: [ ResourceContents{ uri: uri mime_type: 'text/plain' text: 'guide contents' }, ] } })! server.add_resource_template(ResourceTemplate{ uri_template: 'resource://docs/{slug}' name: 'docs' })! server.add_prompt(Prompt{ name: 'review' description: 'Review some code' arguments: [ PromptArgument{ name: 'code' required: true }, ] }, fn (_ Context, arguments string) !GetPromptResult { assert arguments == '{"code":"fn main() {}"}' return GetPromptResult{ description: 'Review prompt' messages: [ prompt_text_message('user', 'Review this code'), ] } })! init_request := Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{"roots":{}}' client_info: Implementation{ name: 'test-client' version: '0.1.0' } }) } init_dispatch := server.dispatch_message(init_request.encode(), stdio_session_id, .stdio)! assert init_dispatch.has_response init_response := decode_response(init_dispatch.response)! init_result := init_response.decode_result[InitializeResult]()! assert init_result.server_info.name == 'test-server' assert init_result.instructions == 'Be precise.' assert init_result.capabilities == '{"tools":{"listChanged":true},"resources":{"listChanged":true,"subscribe":true},"prompts":{"listChanged":true}}' blocked_dispatch := server.dispatch_message(new_request(2, 'tools/list', empty).encode(), stdio_session_id, .stdio)! blocked_response := decode_response(blocked_dispatch.response)! assert blocked_response.error.code == server_not_initialized.code initialized := server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! assert !initialized.has_response tools_list := server.dispatch_message(new_request(3, 'tools/list', empty).encode(), stdio_session_id, .stdio)! tools_result := decode_response(tools_list.response)!.decode_result[ListToolsResult]()! assert tools_result.tools.len == 1 assert tools_result.tools[0].name == 'say_hello' tool_call := server.dispatch_message(Request{ id: encode_id(4) method: 'tools/call' params: '{"name":"say_hello","arguments":{"name":"V"}}' }.encode(), stdio_session_id, .stdio)! tool_result := decode_response(tool_call.response)!.decode_result[ToolResult]()! assert tool_result.content.contains('Hello, V!') assert !tool_result.is_error resource_list := server.dispatch_message(new_request(5, 'resources/list', empty).encode(), stdio_session_id, .stdio)! resource_list_result := decode_response(resource_list.response)!.decode_result[ListResourcesResult]()! assert resource_list_result.resources.len == 1 assert resource_list_result.resources[0].uri == 'resource://guide' resource_templates := server.dispatch_message(new_request(6, 'resources/templates/list', empty).encode(), stdio_session_id, .stdio)! resource_template_result := decode_response(resource_templates.response)!.decode_result[ListResourceTemplatesResult]()! assert resource_template_result.resource_templates.len == 1 assert resource_template_result.resource_templates[0].uri_template == 'resource://docs/{slug}' resource_read := server.dispatch_message(new_request(7, 'resources/read', ReadResourceParams{ uri: 'resource://guide' }).encode(), stdio_session_id, .stdio)! resource_read_result := decode_response(resource_read.response)!.decode_result[ReadResourceResult]()! assert resource_read_result.contents.len == 1 assert resource_read_result.contents[0].text == 'guide contents' prompts_list := server.dispatch_message(new_request(8, 'prompts/list', empty).encode(), stdio_session_id, .stdio)! prompts_list_result := decode_response(prompts_list.response)!.decode_result[ListPromptsResult]()! assert prompts_list_result.prompts.len == 1 assert prompts_list_result.prompts[0].name == 'review' prompt_get := server.dispatch_message(Request{ id: encode_id(9) method: 'prompts/get' params: '{"name":"review","arguments":{"code":"fn main() {}"}}' }.encode(), stdio_session_id, .stdio)! prompt_result := decode_response(prompt_get.response)!.decode_result[GetPromptResult]()! assert prompt_result.messages.len == 1 assert prompt_result.messages[0].role == 'user' ping := server.dispatch_message(new_request(10, 'ping', empty).encode(), stdio_session_id, .stdio)! ping_result := decode_response(ping.response)!.decode_result[EmptyObject]()! assert ping_result == empty_object } fn test_server_http_sessions_and_delete() { mut server_value := new_server( name: 'http-server' version: '0.0.1' ) mut server := &server_value server.add_tool(Tool{ name: 'ping_tool' }, fn (_ Context, _ string) !ToolResult { return tool_text_result('pong') })! server_thread := spawn server.serve_http('127.0.0.1:0') server.wait_till_running(max_retries: 200, retry_period_ms: 10)! time.sleep(20 * time.millisecond) addr := server.http_server.addr url := 'http://${addr}/mcp' mut init_header := http.new_header(http.HeaderConfig{ key: .content_type value: 'application/json' }, http.HeaderConfig{ key: .accept value: 'application/json' }) init_response := http.fetch( method: .post url: url data: Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'http-client' version: '0.1.0' } }) }.encode() header: init_header )! assert init_response.status_code == 200 session_id := init_response.header.get_custom(mcp_session_id_header) or { assert false return } assert session_id != '' mut notification_header := init_header notification_header.set_custom(mcp_session_id_header, session_id)! notification_response := http.fetch( method: .post url: url data: new_notification('notifications/initialized', empty).encode() header: notification_header )! assert notification_response.status_code == 202 mut list_header := notification_header list_header.set(.accept, 'text/event-stream') list_response := http.fetch( method: .post url: url data: new_request(2, 'tools/list', empty).encode() header: list_header )! assert list_response.status_code == 200 assert list_response.header.get(.content_type)?.starts_with(event_stream_content_type) list_messages := parse_sse_messages(list_response.body)! assert list_messages.len == 1 list_result := decode_response(list_messages[0])!.decode_result[ListToolsResult]()! assert list_result.tools.len == 1 assert list_result.tools[0].name == 'ping_tool' mut delete_header := http.new_header() delete_header.set_custom(mcp_session_id_header, session_id)! delete_response := http.fetch( method: .delete url: url header: delete_header )! assert delete_response.status_code == 200 mut stale_header := init_header stale_header.set_custom(mcp_session_id_header, session_id)! stale_response := http.fetch( method: .post url: url data: new_request(3, 'tools/list', empty).encode() header: stale_header )! assert stale_response.status_code == 404 server.close() server_thread.wait() or {} } fn http_initialize(url string) !(string, http.Header) { mut header := http.new_header(http.HeaderConfig{ key: .content_type value: 'application/json' }, http.HeaderConfig{ key: .accept value: 'application/json, text/event-stream' }) response := http.fetch( method: .post url: url data: Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'spec-test' version: '0.0.1' } }) }.encode() header: header )! if response.status_code != 200 { return error('initialize failed: ${response.status_code}') } session_id := response.header.get_custom(mcp_session_id_header) or { return error('missing session id') } return session_id, header } fn test_http_rejects_disallowed_origin() { mut server_value := new_server( name: 'origin-server' version: '0.0.1' allowed_origins: ['https://allowed.example'] ) mut server := &server_value server_thread := spawn server.serve_http('127.0.0.1:0') server.wait_till_running(max_retries: 200, retry_period_ms: 10)! time.sleep(20 * time.millisecond) url := 'http://${server.http_server.addr}/mcp' mut header := http.new_header(http.HeaderConfig{ key: .content_type value: 'application/json' }, http.HeaderConfig{ key: .accept value: 'application/json, text/event-stream' }) header.set_custom('Origin', 'https://attacker.example')! response := http.fetch( method: .post url: url data: Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'attacker' version: '0.0.1' } }) }.encode() header: header )! assert response.status_code == 403 server.close() server_thread.wait() or {} } fn test_http_rejects_unsupported_protocol_version_header() { mut server_value := new_server( name: 'version-server' version: '0.0.1' ) mut server := &server_value server.add_tool(Tool{ name: 'noop' }, fn (_ Context, _ string) !ToolResult { return tool_text_result('ok') })! server_thread := spawn server.serve_http('127.0.0.1:0') server.wait_till_running(max_retries: 200, retry_period_ms: 10)! time.sleep(20 * time.millisecond) url := 'http://${server.http_server.addr}/mcp' session_id, mut header := http_initialize(url)! header.set_custom(mcp_session_id_header, session_id)! header.set_custom(mcp_protocol_version_header, '1999-01-01')! response := http.fetch( method: .post url: url data: new_notification('notifications/initialized', empty).encode() header: header )! assert response.status_code == 400 server.close() server_thread.wait() or {} } fn test_http_rejects_unacceptable_accept_header() { mut server_value := new_server( name: 'accept-server' version: '0.0.1' ) mut server := &server_value server_thread := spawn server.serve_http('127.0.0.1:0') server.wait_till_running(max_retries: 200, retry_period_ms: 10)! time.sleep(20 * time.millisecond) url := 'http://${server.http_server.addr}/mcp' mut header := http.new_header(http.HeaderConfig{ key: .content_type value: 'application/json' }, http.HeaderConfig{ key: .accept value: 'image/png' }) response := http.fetch( method: .post url: url data: new_request(1, 'ping', empty).encode() header: header )! assert response.status_code == 406 server.close() server_thread.wait() or {} } fn test_tool_annotations_are_serialized() { mut server := new_server(name: 'annot', version: '0') server.add_tool(Tool{ name: 'annotated' description: 'demo' annotations: ToolAnnotations{ title: 'Read-only fetcher' read_only_hint: true open_world_hint: false } }, fn (_ Context, _ string) !ToolResult { return tool_text_result('ok') })! encoded := encode_tool(server.tools['annotated'].tool) assert encoded.contains('"annotations":{"title":"Read-only fetcher","readOnlyHint":true,"openWorldHint":false}') } fn test_list_changed_notifications_are_queued_after_initialize() { mut server := new_server(name: 'listchanged', version: '0') init_request := Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) } server.dispatch_message(init_request.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! server.add_tool(Tool{ name: 'late' }, fn (_ Context, _ string) !ToolResult { return tool_text_result('ok') })! server.add_resource(Resource{ uri: 'res://x', name: 'x' }, fn (_ Context, uri string) !ReadResourceResult { return ReadResourceResult{ contents: [ResourceContents{ uri: uri text: 'x' }] } })! server.add_prompt(Prompt{ name: 'late_prompt' }, fn (_ Context, _ string) !GetPromptResult { return GetPromptResult{} })! queue := server.drain_session_notifications(stdio_session_id) assert queue.len == 3 assert decode_notification(queue[0])!.method == 'notifications/tools/list_changed' assert decode_notification(queue[1])!.method == 'notifications/resources/list_changed' assert decode_notification(queue[2])!.method == 'notifications/prompts/list_changed' } fn test_resources_subscribe_then_updated_notifies_only_subscribers() { mut server := new_server(name: 'sub', version: '0') server.add_resource(Resource{ uri: 'res://x', name: 'x' }, fn (_ Context, uri string) !ReadResourceResult { return ReadResourceResult{ contents: [ResourceContents{ uri: uri text: 'x' }] } })! init_request := Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) } server.dispatch_message(init_request.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! subscribe := server.dispatch_message(new_request(2, 'resources/subscribe', SubscribeParams{ uri: 'res://x' }).encode(), stdio_session_id, .stdio)! assert decode_response(subscribe.response)!.error.code == 0 server.notify_resource_updated('res://x') server.notify_resource_updated('res://other') queue := server.drain_session_notifications(stdio_session_id) assert queue.len == 1 notif := decode_notification(queue[0])! assert notif.method == 'notifications/resources/updated' assert notif.params.contains('"uri":"res://x"') unsubscribe := server.dispatch_message(new_request(3, 'resources/unsubscribe', SubscribeParams{ uri: 'res://x' }).encode(), stdio_session_id, .stdio)! assert decode_response(unsubscribe.response)!.error.code == 0 server.notify_resource_updated('res://x') assert server.drain_session_notifications(stdio_session_id).len == 0 } fn test_logging_set_level_filters_messages_below_threshold() { mut server := new_server(name: 'log', version: '0', enable_logging: true) init_request := Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) } init_dispatch := server.dispatch_message(init_request.encode(), stdio_session_id, .stdio)! init_response := decode_response(init_dispatch.response)! init_result := init_response.decode_result[InitializeResult]()! assert init_result.capabilities.contains('"logging":{}') server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! set_level := server.dispatch_message(new_request(2, 'logging/setLevel', SetLevelParams{ level: 'warning' }).encode(), stdio_session_id, .stdio)! assert decode_response(set_level.response)!.error.code == 0 server.notify_log(.debug, 'svc', '"low"') server.notify_log(.warning, 'svc', '"hi"') server.notify_log(.error, 'svc', '{"k":1}') queue := server.drain_session_notifications(stdio_session_id) assert queue.len == 2 first := decode_notification(queue[0])! assert first.method == 'notifications/message' assert first.params.contains('"level":"warning"') assert first.params.contains('"logger":"svc"') second := decode_notification(queue[1])! assert second.params.contains('"level":"error"') assert second.params.contains('"data":{"k":1}') } fn test_logging_set_level_unknown_returns_invalid_params() { mut server := new_server(name: 'log', version: '0', enable_logging: true) server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! dispatch := server.dispatch_message(new_request(2, 'logging/setLevel', SetLevelParams{ level: 'fatal' }).encode(), stdio_session_id, .stdio)! assert decode_response(dispatch.response)!.error.code == invalid_params.code } fn test_logging_disabled_rejects_set_level() { mut server := new_server(name: 'log', version: '0') server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! dispatch := server.dispatch_message(new_request(2, 'logging/setLevel', SetLevelParams{ level: 'info' }).encode(), stdio_session_id, .stdio)! assert decode_response(dispatch.response)!.error.code == method_not_found.code } fn test_progress_token_is_extracted_and_notification_is_sent() { mut server := new_server(name: 'p', version: '0') server.add_tool(Tool{ name: 'work' }, fn (ctx Context, _ string) !ToolResult { ctx.notify_progress(0.25, 1.0, 'starting') ctx.notify_progress(1.0, 1.0, 'done') return tool_text_result('done') })! server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! server.dispatch_message('{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"work","arguments":{},"_meta":{"progressToken":"abc"}}}', stdio_session_id, .stdio)! queue := server.drain_session_notifications(stdio_session_id) assert queue.len == 2 first := decode_notification(queue[0])! assert first.method == 'notifications/progress' assert first.params.contains('"progressToken":"abc"') assert first.params.contains('"progress":0.25') assert first.params.contains('"total":1') assert first.params.contains('"message":"starting"') } fn test_cancellation_marks_request_until_cleared() { mut server := new_server(name: 'c', version: '0') server.add_tool(Tool{ name: 'check' }, fn (ctx Context, _ string) !ToolResult { assert ctx.is_cancelled() return tool_text_result('seen') })! server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! // Request ids carry their JSON form unchanged (string vs number) so the // cancellation must use the exact same form per spec. server.dispatch_message('{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":7,"reason":"user"}}', stdio_session_id, .stdio)! assert server.is_request_cancelled(stdio_session_id, '7') server.dispatch_message('{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"check","arguments":{}}}', stdio_session_id, .stdio)! assert !server.is_request_cancelled(stdio_session_id, '7') } fn test_completion_complete_routes_to_registered_handler() { mut server := new_server(name: 'cplt', version: '0') server.add_prompt(Prompt{ name: 'review' arguments: [PromptArgument{ name: 'lang' }] }, fn (_ Context, _ string) !GetPromptResult { return GetPromptResult{} })! server.add_completion(CompletionRef{ ref_type: 'ref/prompt', name: 'review' }, 'lang', fn (_ Context, current_value string, _ string) !CompletionResult { candidates := ['rust', 'python', 'go', 'v'] matches := candidates.filter(it.starts_with(current_value)) return CompletionResult{ values: matches total: matches.len has_more: false } })! server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! dispatch := server.dispatch_message('{"jsonrpc":"2.0","id":2,"method":"completion/complete","params":{"ref":{"type":"ref/prompt","name":"review"},"argument":{"name":"lang","value":"r"}}}', stdio_session_id, .stdio)! body := decode_response(dispatch.response)!.result assert body.contains('"values":["rust"]') assert body.contains('"total":1') assert body.contains('"hasMore":false') } fn test_completion_unknown_handler_returns_empty_values() { mut server := new_server(name: 'cplt', version: '0') server.add_completion(CompletionRef{ ref_type: 'ref/resource', uri: 'res://a' }, 'k', fn (_ Context, _ string, _ string) !CompletionResult { return CompletionResult{ values: ['x'] } })! server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! dispatch := server.dispatch_message('{"jsonrpc":"2.0","id":2,"method":"completion/complete","params":{"ref":{"type":"ref/resource","uri":"res://other"},"argument":{"name":"k","value":""}}}', stdio_session_id, .stdio)! body := decode_response(dispatch.response)!.result assert body == '{"completion":{"values":[]}}' } fn test_http_get_streams_queued_notifications_with_event_ids() { mut server_value := new_server(name: 'sse', version: '0', enable_logging: true) mut server := &server_value server.add_resource(Resource{ uri: 'res://x', name: 'x' }, fn (_ Context, uri string) !ReadResourceResult { return ReadResourceResult{ contents: [ResourceContents{ uri: uri text: 'x' }] } })! server_thread := spawn server.serve_http('127.0.0.1:0') server.wait_till_running(max_retries: 200, retry_period_ms: 10)! time.sleep(20 * time.millisecond) url := 'http://${server.http_server.addr}/mcp' session_id, mut header := http_initialize(url)! header.set_custom(mcp_session_id_header, session_id)! notification_response := http.fetch( method: .post url: url data: new_notification('notifications/initialized', empty).encode() header: header )! assert notification_response.status_code == 202 server.notify_log(.info, 'svc', '"hello"') server.notify_log(.warning, 'svc', '"world"') mut get_header := http.new_header() get_header.set(.accept, 'text/event-stream') get_header.set_custom(mcp_session_id_header, session_id)! stream := http.fetch( method: .get url: url header: get_header )! assert stream.status_code == 200 assert stream.header.get(.content_type)?.starts_with(event_stream_content_type) assert stream.body.contains('id: 1') assert stream.body.contains('id: 2') first_messages := parse_sse_messages(stream.body)! assert first_messages.len == 2 assert decode_notification(first_messages[0])!.method == 'notifications/message' mut resume_header := get_header resume_header.set_custom(last_event_id_header, '1')! resume := http.fetch( method: .get url: url header: resume_header )! assert resume.body.contains('id: 2') assert !resume.body.contains('id: 1\n') server.close() server_thread.wait() or {} } fn test_server_initiated_list_roots_round_trip() { mut server := new_server(name: 'roots', version: '0') server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! mut request_thread := spawn fn [mut server] () !ListRootsResult { return server.list_roots(stdio_session_id, 1 * time.second) }() // Drain the queued request and respond as a client would. mut server_request_id := '' deadline := time.now().add(1 * time.second) for time.now() < deadline { queued := server.drain_session_notifications(stdio_session_id) if queued.len != 0 { req := decode_request(queued[0])! assert req.method == 'roots/list' server_request_id = req.id break } time.sleep(2 * time.millisecond) } assert server_request_id != '' server.dispatch_message('{"jsonrpc":"2.0","id":${server_request_id},"result":{"roots":[{"uri":"file:///tmp","name":"tmp"}]}}', stdio_session_id, .stdio)! roots := request_thread.wait()! assert roots.roots.len == 1 assert roots.roots[0].uri == 'file:///tmp' } fn test_server_initiated_request_returns_on_timeout() { mut server := new_server(name: 'roots', version: '0') server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! started := time.now() server.list_roots(stdio_session_id, 50 * time.millisecond) or { // `timed_wait` should return promptly after the deadline; allow up to // 10x the timeout to account for CI scheduler jitter. elapsed := time.now() - started assert elapsed < 500 * time.millisecond assert err.msg().contains('timeout') return } assert false, 'list_roots should have timed out' } fn test_late_response_after_timeout_does_not_leak_pending() { mut server := new_server(name: 'late', version: '0') server.dispatch_message(Request{ id: encode_id(1) method: 'initialize' params: encode_initialize_params(InitializeParams{ protocol_version: protocol_version capabilities: '{}' client_info: Implementation{ name: 'c' version: '0' } }) }.encode(), stdio_session_id, .stdio)! server.dispatch_message(new_notification('notifications/initialized', empty).encode(), stdio_session_id, .stdio)! server.list_roots(stdio_session_id, 30 * time.millisecond) or { assert err.msg().contains('timeout') } // The waiter has cleaned up its semaphore; a late reply must be dropped // rather than parked in `pending_responses` forever. mut server_request_id := '' rlock server.state { session := server.state.sessions[stdio_session_id] server_request_id = '"server-${session.next_request_seq - 1}"' } server.dispatch_message('{"jsonrpc":"2.0","id":${server_request_id},"result":{"roots":[]}}', stdio_session_id, .stdio)! rlock server.state { session := server.state.sessions[stdio_session_id] assert session.pending_responses.len == 0 } } fn test_http_get_resume_drains_queue_before_replay() { mut server_value := new_server(name: 'resume', version: '0', enable_logging: true) mut server := &server_value server_thread := spawn server.serve_http('127.0.0.1:0') server.wait_till_running(max_retries: 200, retry_period_ms: 10)! time.sleep(20 * time.millisecond) url := 'http://${server.http_server.addr}/mcp' session_id, mut header := http_initialize(url)! header.set_custom(mcp_session_id_header, session_id)! notification_response := http.fetch( method: .post url: url data: new_notification('notifications/initialized', empty).encode() header: header )! assert notification_response.status_code == 202 // First batch: drained on the initial GET, assigns ids 1..2. server.notify_log(.info, 'svc', '"first"') server.notify_log(.info, 'svc', '"second"') mut get_header := http.new_header() get_header.set(.accept, 'text/event-stream') get_header.set_custom(mcp_session_id_header, session_id)! first := http.fetch(method: .get, url: url, header: get_header)! assert first.status_code == 200 assert first.body.contains('id: 1') assert first.body.contains('id: 2') // Second batch arrives while the client is between GETs — it stays in the // notification queue and would be missed if the resume only replayed the // event log without draining first. server.notify_log(.info, 'svc', '"third"') server.notify_log(.info, 'svc', '"fourth"') mut resume_header := get_header resume_header.set_custom(last_event_id_header, '2')! resume := http.fetch(method: .get, url: url, header: resume_header)! assert resume.status_code == 200 assert resume.body.contains('id: 3') assert resume.body.contains('id: 4') assert resume.body.contains('"third"') assert resume.body.contains('"fourth"') server.close() server_thread.wait() or {} } fn test_http_returns_json_when_accept_lists_both() { mut server_value := new_server( name: 'json-default-server' version: '0.0.1' ) mut server := &server_value server_thread := spawn server.serve_http('127.0.0.1:0') server.wait_till_running(max_retries: 200, retry_period_ms: 10)! time.sleep(20 * time.millisecond) url := 'http://${server.http_server.addr}/mcp' session_id, mut header := http_initialize(url)! header.set_custom(mcp_session_id_header, session_id)! notification_response := http.fetch( method: .post url: url data: new_notification('notifications/initialized', empty).encode() header: header )! assert notification_response.status_code == 202 ping_response := http.fetch( method: .post url: url data: new_request(2, 'ping', empty).encode() header: header )! assert ping_response.status_code == 200 assert ping_response.header.get(.content_type)?.starts_with(default_content_type) server.close() server_thread.wait() or {} }