| 1 | module mcp |
| 2 | |
| 3 | import net.http |
| 4 | import time |
| 5 | |
| 6 | fn test_server_routes_initialize_and_registered_features() { |
| 7 | mut server := new_server( |
| 8 | name: 'test-server' |
| 9 | version: '1.2.3' |
| 10 | instructions: 'Be precise.' |
| 11 | ) |
| 12 | server.add_tool(Tool{ |
| 13 | name: 'say_hello' |
| 14 | description: 'Returns a greeting' |
| 15 | }, fn (ctx Context, arguments string) !ToolResult { |
| 16 | assert ctx.session_id == stdio_session_id |
| 17 | assert ctx.transport == .stdio |
| 18 | assert arguments == '{"name":"V"}' |
| 19 | return tool_text_result('Hello, V!') |
| 20 | })! |
| 21 | server.add_resource(Resource{ |
| 22 | uri: 'resource://guide' |
| 23 | name: 'guide' |
| 24 | mime_type: 'text/plain' |
| 25 | }, fn (_ Context, uri string) !ReadResourceResult { |
| 26 | return ReadResourceResult{ |
| 27 | contents: [ |
| 28 | ResourceContents{ |
| 29 | uri: uri |
| 30 | mime_type: 'text/plain' |
| 31 | text: 'guide contents' |
| 32 | }, |
| 33 | ] |
| 34 | } |
| 35 | })! |
| 36 | server.add_resource_template(ResourceTemplate{ |
| 37 | uri_template: 'resource://docs/{slug}' |
| 38 | name: 'docs' |
| 39 | })! |
| 40 | server.add_prompt(Prompt{ |
| 41 | name: 'review' |
| 42 | description: 'Review some code' |
| 43 | arguments: [ |
| 44 | PromptArgument{ |
| 45 | name: 'code' |
| 46 | required: true |
| 47 | }, |
| 48 | ] |
| 49 | }, fn (_ Context, arguments string) !GetPromptResult { |
| 50 | assert arguments == '{"code":"fn main() {}"}' |
| 51 | return GetPromptResult{ |
| 52 | description: 'Review prompt' |
| 53 | messages: [ |
| 54 | prompt_text_message('user', 'Review this code'), |
| 55 | ] |
| 56 | } |
| 57 | })! |
| 58 | |
| 59 | init_request := Request{ |
| 60 | id: encode_id(1) |
| 61 | method: 'initialize' |
| 62 | params: encode_initialize_params(InitializeParams{ |
| 63 | protocol_version: protocol_version |
| 64 | capabilities: '{"roots":{}}' |
| 65 | client_info: Implementation{ |
| 66 | name: 'test-client' |
| 67 | version: '0.1.0' |
| 68 | } |
| 69 | }) |
| 70 | } |
| 71 | init_dispatch := server.dispatch_message(init_request.encode(), stdio_session_id, .stdio)! |
| 72 | assert init_dispatch.has_response |
| 73 | init_response := decode_response(init_dispatch.response)! |
| 74 | init_result := init_response.decode_result[InitializeResult]()! |
| 75 | assert init_result.server_info.name == 'test-server' |
| 76 | assert init_result.instructions == 'Be precise.' |
| 77 | assert init_result.capabilities == '{"tools":{},"resources":{},"prompts":{}}' |
| 78 | |
| 79 | blocked_dispatch := server.dispatch_message(new_request(2, 'tools/list', empty).encode(), |
| 80 | stdio_session_id, .stdio)! |
| 81 | blocked_response := decode_response(blocked_dispatch.response)! |
| 82 | assert blocked_response.error.code == server_not_initialized.code |
| 83 | |
| 84 | initialized := server.dispatch_message(new_notification('notifications/initialized', empty).encode(), |
| 85 | stdio_session_id, .stdio)! |
| 86 | assert !initialized.has_response |
| 87 | |
| 88 | tools_list := server.dispatch_message(new_request(3, 'tools/list', empty).encode(), |
| 89 | stdio_session_id, .stdio)! |
| 90 | tools_result := decode_response(tools_list.response)!.decode_result[ListToolsResult]()! |
| 91 | assert tools_result.tools.len == 1 |
| 92 | assert tools_result.tools[0].name == 'say_hello' |
| 93 | |
| 94 | tool_call := server.dispatch_message(Request{ |
| 95 | id: encode_id(4) |
| 96 | method: 'tools/call' |
| 97 | params: '{"name":"say_hello","arguments":{"name":"V"}}' |
| 98 | }.encode(), stdio_session_id, .stdio)! |
| 99 | tool_result := decode_response(tool_call.response)!.decode_result[ToolResult]()! |
| 100 | assert tool_result.content.contains('Hello, V!') |
| 101 | assert !tool_result.is_error |
| 102 | |
| 103 | resource_list := server.dispatch_message(new_request(5, 'resources/list', empty).encode(), |
| 104 | stdio_session_id, .stdio)! |
| 105 | resource_list_result := |
| 106 | decode_response(resource_list.response)!.decode_result[ListResourcesResult]()! |
| 107 | assert resource_list_result.resources.len == 1 |
| 108 | assert resource_list_result.resources[0].uri == 'resource://guide' |
| 109 | |
| 110 | resource_templates := server.dispatch_message(new_request(6, 'resources/templates/list', empty).encode(), |
| 111 | stdio_session_id, .stdio)! |
| 112 | resource_template_result := |
| 113 | decode_response(resource_templates.response)!.decode_result[ListResourceTemplatesResult]()! |
| 114 | assert resource_template_result.resource_templates.len == 1 |
| 115 | assert resource_template_result.resource_templates[0].uri_template == 'resource://docs/{slug}' |
| 116 | |
| 117 | resource_read := server.dispatch_message(new_request(7, 'resources/read', ReadResourceParams{ |
| 118 | uri: 'resource://guide' |
| 119 | }).encode(), stdio_session_id, .stdio)! |
| 120 | resource_read_result := |
| 121 | decode_response(resource_read.response)!.decode_result[ReadResourceResult]()! |
| 122 | assert resource_read_result.contents.len == 1 |
| 123 | assert resource_read_result.contents[0].text == 'guide contents' |
| 124 | |
| 125 | prompts_list := server.dispatch_message(new_request(8, 'prompts/list', empty).encode(), |
| 126 | stdio_session_id, .stdio)! |
| 127 | prompts_list_result := |
| 128 | decode_response(prompts_list.response)!.decode_result[ListPromptsResult]()! |
| 129 | assert prompts_list_result.prompts.len == 1 |
| 130 | assert prompts_list_result.prompts[0].name == 'review' |
| 131 | |
| 132 | prompt_get := server.dispatch_message(Request{ |
| 133 | id: encode_id(9) |
| 134 | method: 'prompts/get' |
| 135 | params: '{"name":"review","arguments":{"code":"fn main() {}"}}' |
| 136 | }.encode(), stdio_session_id, .stdio)! |
| 137 | prompt_result := decode_response(prompt_get.response)!.decode_result[GetPromptResult]()! |
| 138 | assert prompt_result.messages.len == 1 |
| 139 | assert prompt_result.messages[0].role == 'user' |
| 140 | |
| 141 | ping := server.dispatch_message(new_request(10, 'ping', empty).encode(), stdio_session_id, |
| 142 | .stdio)! |
| 143 | ping_result := decode_response(ping.response)!.decode_result[EmptyObject]()! |
| 144 | assert ping_result == empty_object |
| 145 | } |
| 146 | |
| 147 | fn test_server_http_sessions_and_delete() { |
| 148 | mut server_value := new_server( |
| 149 | name: 'http-server' |
| 150 | version: '0.0.1' |
| 151 | ) |
| 152 | mut server := &server_value |
| 153 | server.add_tool(Tool{ |
| 154 | name: 'ping_tool' |
| 155 | }, fn (_ Context, _ string) !ToolResult { |
| 156 | return tool_text_result('pong') |
| 157 | })! |
| 158 | |
| 159 | server_thread := spawn server.serve_http('127.0.0.1:0') |
| 160 | server.wait_till_running(max_retries: 200, retry_period_ms: 10)! |
| 161 | time.sleep(20 * time.millisecond) |
| 162 | addr := server.http_server.addr |
| 163 | url := 'http://${addr}/mcp' |
| 164 | |
| 165 | mut init_header := http.new_header(http.HeaderConfig{ |
| 166 | key: .content_type |
| 167 | value: 'application/json' |
| 168 | }, http.HeaderConfig{ |
| 169 | key: .accept |
| 170 | value: 'application/json' |
| 171 | }) |
| 172 | init_response := http.fetch( |
| 173 | method: .post |
| 174 | url: url |
| 175 | data: Request{ |
| 176 | id: encode_id(1) |
| 177 | method: 'initialize' |
| 178 | params: encode_initialize_params(InitializeParams{ |
| 179 | protocol_version: protocol_version |
| 180 | capabilities: '{}' |
| 181 | client_info: Implementation{ |
| 182 | name: 'http-client' |
| 183 | version: '0.1.0' |
| 184 | } |
| 185 | }) |
| 186 | }.encode() |
| 187 | header: init_header |
| 188 | )! |
| 189 | assert init_response.status_code == 200 |
| 190 | session_id := init_response.header.get_custom(mcp_session_id_header) or { |
| 191 | assert false |
| 192 | return |
| 193 | } |
| 194 | assert session_id != '' |
| 195 | |
| 196 | mut notification_header := init_header |
| 197 | notification_header.set_custom(mcp_session_id_header, session_id)! |
| 198 | notification_response := http.fetch( |
| 199 | method: .post |
| 200 | url: url |
| 201 | data: new_notification('notifications/initialized', empty).encode() |
| 202 | header: notification_header |
| 203 | )! |
| 204 | assert notification_response.status_code == 202 |
| 205 | |
| 206 | mut list_header := notification_header |
| 207 | list_header.set(.accept, 'text/event-stream') |
| 208 | list_response := http.fetch( |
| 209 | method: .post |
| 210 | url: url |
| 211 | data: new_request(2, 'tools/list', empty).encode() |
| 212 | header: list_header |
| 213 | )! |
| 214 | assert list_response.status_code == 200 |
| 215 | assert list_response.header.get(.content_type)!.starts_with(event_stream_content_type) |
| 216 | list_messages := parse_sse_messages(list_response.body)! |
| 217 | assert list_messages.len == 1 |
| 218 | list_result := decode_response(list_messages[0])!.decode_result[ListToolsResult]()! |
| 219 | assert list_result.tools.len == 1 |
| 220 | assert list_result.tools[0].name == 'ping_tool' |
| 221 | |
| 222 | mut delete_header := http.new_header() |
| 223 | delete_header.set_custom(mcp_session_id_header, session_id)! |
| 224 | delete_response := http.fetch( |
| 225 | method: .delete |
| 226 | url: url |
| 227 | header: delete_header |
| 228 | )! |
| 229 | assert delete_response.status_code == 200 |
| 230 | |
| 231 | mut stale_header := init_header |
| 232 | stale_header.set_custom(mcp_session_id_header, session_id)! |
| 233 | stale_response := http.fetch( |
| 234 | method: .post |
| 235 | url: url |
| 236 | data: new_request(3, 'tools/list', empty).encode() |
| 237 | header: stale_header |
| 238 | )! |
| 239 | assert stale_response.status_code == 404 |
| 240 | server.close() |
| 241 | server_thread.wait() or {} |
| 242 | } |
| 243 | |