v2 / vlib / mcp / server_test.v
242 lines · 222 sloc · 7.8 KB · 26926f194fd79184bc6758f8ece86e5c4a6ab44d
Raw
1module mcp
2
3import net.http
4import time
5
6fn 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
147fn 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