v / vlib / mcp / mcp.v
911 lines · 821 sloc · 24.74 KB · 7b55539b6e355cd5930ad6213fc49d3c884821dc
Raw
1module mcp
2
3import json
4import net.http
5import os
6import time
7
8pub const jsonrpc_version = '2.0'
9pub const protocol_version = '2025-11-25'
10pub const parse_error = ResponseError{
11 code: -32700
12 message: 'Invalid JSON.'
13}
14pub const invalid_request = ResponseError{
15 code: -32600
16 message: 'Invalid request.'
17}
18pub const method_not_found = ResponseError{
19 code: -32601
20 message: 'Method not found.'
21}
22pub const invalid_params = ResponseError{
23 code: -32602
24 message: 'Invalid params.'
25}
26pub const internal_error = ResponseError{
27 code: -32603
28 message: 'Internal error.'
29}
30pub const server_not_initialized = ResponseError{
31 code: -32002
32 message: 'Server not initialized.'
33}
34pub const resource_not_found = ResponseError{
35 code: -32002
36 message: 'Resource not found.'
37}
38pub const url_elicitation_required = ResponseError{
39 code: -32042
40 message: 'URL mode elicitation required.'
41}
42
43const default_content_type = 'application/json'
44const event_stream_content_type = 'text/event-stream'
45const streamable_http_accept = '${default_content_type}, ${event_stream_content_type}'
46const mcp_session_id_header = 'MCP-Session-Id'
47const mcp_protocol_version_header = 'MCP-Protocol-Version'
48const last_event_id_header = 'Last-Event-ID'
49const default_protocol_version = '2025-03-26'
50const process_poll_interval = 5 * time.millisecond
51const default_client_name = 'v.mcp'
52const default_client_version = 'dev'
53
54// ResponseError is the JSON-RPC error payload used by MCP responses.
55pub struct ResponseError {
56pub:
57 code int
58 message string
59 data string @[raw]
60}
61
62// code returns the JSON-RPC error code.
63pub fn (err ResponseError) code() int {
64 return err.code
65}
66
67// msg returns the JSON-RPC error message.
68pub fn (err ResponseError) msg() string {
69 return err.message
70}
71
72// err casts the response error to `IError`.
73pub fn (err ResponseError) err() IError {
74 return IError(err)
75}
76
77// Null represents the JSON `null` literal.
78pub struct Null {}
79
80// str returns the JSON `null` literal.
81pub fn (n Null) str() string {
82 return 'null'
83}
84
85pub const null = Null{}
86
87// Empty omits a JSON-RPC field when used with MCP helpers.
88pub struct Empty {}
89
90// str returns the empty string.
91pub fn (e Empty) str() string {
92 return ''
93}
94
95pub const empty = Empty{}
96
97// EmptyObject encodes to an empty JSON object.
98pub struct EmptyObject {}
99
100// str returns the JSON empty object literal.
101pub fn (e EmptyObject) str() string {
102 return '{}'
103}
104
105pub const empty_object = EmptyObject{}
106
107// Icon describes a UI icon advertised by an MCP implementation, tool,
108// resource, resource template or prompt. `src` is required; `mime_type`,
109// `sizes` and `theme` are optional metadata mirroring the spec's `Icon` shape.
110pub struct Icon {
111pub:
112 src string
113 mime_type string @[json: mimeType; omitempty]
114 sizes []string @[omitempty]
115 theme string @[omitempty]
116}
117
118// Implementation identifies an MCP client or server implementation. `name`
119// and `version` are required; `title`, `description`, `website_url` and
120// `icons` are optional 2025-11-25 metadata extensions (BaseMetadata + Icons).
121pub struct Implementation {
122pub:
123 name string
124 version string
125 title string @[omitempty]
126 description string @[omitempty]
127 website_url string @[json: websiteUrl; omitempty]
128 icons []Icon @[omitempty]
129}
130
131// InitializeParams is the typed payload for the `initialize` request.
132pub struct InitializeParams {
133pub:
134 protocol_version string @[json: protocolVersion]
135 capabilities string @[raw]
136 client_info Implementation @[json: clientInfo]
137}
138
139// InitializeResult is the typed result returned by an MCP server after initialization.
140pub struct InitializeResult {
141pub:
142 protocol_version string @[json: protocolVersion]
143 capabilities string @[raw]
144 server_info Implementation @[json: serverInfo]
145 instructions string
146}
147
148// Request is a JSON-RPC request message encoded for MCP.
149pub struct Request {
150pub:
151 jsonrpc string = jsonrpc_version
152 id string @[raw]
153 method string
154 params string @[omitempty; raw]
155}
156
157// new_request constructs an MCP request with a typed id and params payload.
158pub fn new_request[I, P](id I, method string, params P) Request {
159 return Request{
160 id: encode_id(id)
161 method: method
162 params: encode_value(params)
163 }
164}
165
166// encode serializes the request to JSON.
167pub fn (req Request) encode() string {
168 params_payload := if req.params.len == 0 { '' } else { ',"params":${req.params}' }
169 id_payload := if req.id.len == 0 { null.str() } else { req.id }
170 return '{"jsonrpc":"${jsonrpc_version}","id":${id_payload},"method":${json.encode(req.method)}${params_payload}}'
171}
172
173// decode_params decodes the raw request params into `T`.
174pub fn (req Request) decode_params[T]() !T {
175 return decode_value[T](req.params)
176}
177
178// Notification is a JSON-RPC notification encoded for MCP.
179pub struct Notification {
180pub:
181 jsonrpc string = jsonrpc_version
182 method string
183 params string @[omitempty; raw]
184}
185
186// new_notification constructs an MCP notification with a typed params payload.
187pub fn new_notification[P](method string, params P) Notification {
188 return Notification{
189 method: method
190 params: encode_value(params)
191 }
192}
193
194// encode serializes the notification to JSON.
195pub fn (notification Notification) encode() string {
196 params_payload := if notification.params.len == 0 {
197 ''
198 } else {
199 ',"params":${notification.params}'
200 }
201 return '{"jsonrpc":"${jsonrpc_version}","method":${json.encode(notification.method)}${params_payload}}'
202}
203
204// decode_params decodes the raw notification params into `T`.
205pub fn (notification Notification) decode_params[T]() !T {
206 return decode_value[T](notification.params)
207}
208
209// Response is a JSON-RPC response message encoded for MCP.
210pub struct Response {
211pub:
212 jsonrpc string = jsonrpc_version
213 id string @[raw]
214 result string @[raw]
215 error ResponseError
216}
217
218// new_response constructs an MCP response with a typed id and result payload.
219pub fn new_response[I, R](id I, result R, err ResponseError) Response {
220 return Response{
221 id: encode_id(id)
222 result: if err.code != 0 { '' } else { encode_value(result) }
223 error: err
224 }
225}
226
227// encode serializes the response to JSON.
228pub fn (resp Response) encode() string {
229 mut payload := '{"jsonrpc":"${jsonrpc_version}"'
230 if resp.error.code != 0 {
231 payload += ',"error":' + encode_response_error(resp.error)
232 } else {
233 result_payload := if resp.result.len == 0 { null.str() } else { resp.result }
234 payload += ',"result":' + result_payload
235 }
236 id_payload := if resp.id.len == 0 { null.str() } else { resp.id }
237 return payload + ',"id":${id_payload}}'
238}
239
240// encode_response_error renders a ResponseError as JSON, preserving the
241// `data` payload as raw JSON. V's `json.encode` ignores the `@[raw]` tag on
242// encode, so a hand-rolled writer is required to keep the wire shape spec
243// compliant (the `data` field MAY be any JSON value per JSON-RPC 2.0).
244fn encode_response_error(err ResponseError) string {
245 mut fields := ['"code":${err.code}', '"message":${json.encode(err.message)}']
246 if err.data.trim_space() != '' {
247 fields << '"data":${err.data}'
248 }
249 return '{${fields.join(',')}}'
250}
251
252// decode_result decodes the response result into `T`.
253pub fn (resp Response) decode_result[T]() !T {
254 if resp.error.code != 0 {
255 return resp.error.err()
256 }
257 return decode_value[T](resp.result)
258}
259
260// decode_request decodes a JSON payload into an MCP request.
261pub fn decode_request(raw string) !Request {
262 return json.decode(Request, raw) or { return err }
263}
264
265// decode_notification decodes a JSON payload into an MCP notification.
266pub fn decode_notification(raw string) !Notification {
267 return json.decode(Notification, raw) or { return err }
268}
269
270// decode_response decodes a JSON payload into an MCP response.
271pub fn decode_response(raw string) !Response {
272 return json.decode(Response, raw) or { return err }
273}
274
275struct MessageEnvelope {
276 jsonrpc string
277 id string @[raw]
278 method string
279 params string @[raw]
280 result string @[raw]
281 error ResponseError
282}
283
284// is_notification_id reports whether a JSON-RPC `id` field encodes the
285// "absent or null" form, which per spec marks the envelope as a notification.
286fn is_notification_id(id string) bool {
287 return id == '' || id == null.str()
288}
289
290fn (env MessageEnvelope) encode() string {
291 if env.method.len != 0 {
292 if is_notification_id(env.id) {
293 return Notification{
294 method: env.method
295 params: env.params
296 }.encode()
297 }
298 return Request{
299 id: env.id
300 method: env.method
301 params: env.params
302 }.encode()
303 }
304 return Response{
305 id: env.id
306 result: env.result
307 error: env.error
308 }.encode()
309}
310
311fn decode_envelope(raw string) !MessageEnvelope {
312 return json.decode(MessageEnvelope, raw) or { return err }
313}
314
315// Transport is the boundary between MCP messages and the wire format.
316pub interface Transport {
317mut:
318 send(message string) !
319 receive() !string
320 close()
321}
322
323@[params]
324pub struct ClientConfig {
325pub mut:
326 protocol_version string = protocol_version
327 client_info Implementation = Implementation{
328 name: default_client_name
329 version: default_client_version
330 }
331 capabilities string = '{}'
332 headers map[string]string
333}
334
335pub struct Client {
336mut:
337 transport Transport
338 config ClientConfig
339 next_id int = 1
340 initialized bool
341 init_result InitializeResult
342 pending_responses map[string]Response
343 notifications []Notification
344 server_requests []Request
345}
346
347// new_client constructs an MCP client on top of a custom transport.
348pub fn new_client(transport Transport, config ClientConfig) Client {
349 return Client{
350 transport: transport
351 config: config
352 pending_responses: map[string]Response{}
353 }
354}
355
356// connect creates an MCP client for a streamable HTTP endpoint.
357pub fn connect(url string) !Client {
358 return connect_http(url, ClientConfig{})
359}
360
361// connect_http creates an MCP client for a streamable HTTP endpoint.
362pub fn connect_http(url string, config ClientConfig) !Client {
363 transport := new_http_transport(url, config)!
364 return new_client(transport, config)
365}
366
367// connect_stdio creates an MCP client that talks to a local stdio server process.
368pub fn connect_stdio(command string, args []string, config ClientConfig) !Client {
369 transport := new_process_transport(command, args)!
370 return new_client(transport, config)
371}
372
373// initialize starts the MCP initialization handshake using the client's config.
374pub fn (mut c Client) initialize() !InitializeResult {
375 return c.initialize_with_raw(c.config.capabilities, c.config.client_info)
376}
377
378// initialize_with starts the MCP initialization handshake using typed capabilities.
379pub fn (mut c Client) initialize_with[X](capabilities X, client_info Implementation) !InitializeResult {
380 return c.initialize_with_raw(encode_value(capabilities), client_info)
381}
382
383// send_request sends a typed request and waits for its response.
384pub fn (mut c Client) send_request(request Request) !Response {
385 if request.method == 'initialize' {
386 return error('mcp.Client.initialize must be used for the MCP handshake')
387 }
388 c.ensure_initialized()!
389 c.transport.send(request.encode())!
390 return c.wait_for_response(request.id)
391}
392
393// request_message sends a method call and returns the raw MCP response.
394pub fn (mut c Client) request_message[P](method string, params P) !Response {
395 request := new_request(c.next_request_id(), method, params)
396 return c.send_request(request)
397}
398
399// request sends a method call and decodes its result into `Result`.
400pub fn (mut c Client) request[P, R](method string, params P) !R {
401 response := c.request_message(method, params)!
402 result := response.decode_result[R]()!
403 return result
404}
405
406// send_notification sends a typed notification message.
407pub fn (mut c Client) send_notification(notification Notification) ! {
408 if notification.method == 'notifications/initialized' {
409 return error('notifications/initialized is sent automatically after initialization')
410 }
411 c.ensure_initialized()!
412 c.transport.send(notification.encode())!
413}
414
415// notify sends a method notification with a typed params payload.
416pub fn (mut c Client) notify[P](method string, params P) ! {
417 c.send_notification(new_notification(method, params))!
418}
419
420// take_notifications drains notifications queued while waiting for responses.
421pub fn (mut c Client) take_notifications() []Notification {
422 if c.notifications.len == 0 {
423 return []Notification{}
424 }
425 drained := c.notifications.clone()
426 c.notifications = []Notification{}
427 return drained
428}
429
430// take_requests drains server initiated requests queued while waiting for responses.
431pub fn (mut c Client) take_requests() []Request {
432 if c.server_requests.len == 0 {
433 return []Request{}
434 }
435 drained := c.server_requests.clone()
436 c.server_requests = []Request{}
437 return drained
438}
439
440// close releases the underlying transport.
441pub fn (mut c Client) close() {
442 c.transport.close()
443}
444
445fn (mut c Client) initialize_with_raw(capabilities string, client_info Implementation) !InitializeResult {
446 if c.initialized {
447 return c.init_result
448 }
449 c.config.capabilities = normalize_capabilities(capabilities)
450 c.config.client_info = normalize_client_info(client_info)
451 params := InitializeParams{
452 protocol_version: normalize_protocol_version(c.config.protocol_version)
453 capabilities: c.config.capabilities
454 client_info: c.config.client_info
455 }
456 request := Request{
457 id: encode_id(c.next_request_id())
458 method: 'initialize'
459 params: encode_initialize_params(params)
460 }
461 c.transport.send(request.encode())!
462 response := c.wait_for_response(request.id)!
463 result := response.decode_result[InitializeResult]()!
464 c.transport.send(new_notification('notifications/initialized', empty).encode())!
465 c.initialized = true
466 c.init_result = result
467 return result
468}
469
470fn (mut c Client) ensure_initialized() ! {
471 if !c.initialized {
472 c.initialize()!
473 }
474}
475
476fn (mut c Client) next_request_id() int {
477 request_id := c.next_id
478 c.next_id++
479 return request_id
480}
481
482fn (mut c Client) wait_for_response(expected_id string) !Response {
483 if expected_id in c.pending_responses {
484 response := c.pending_responses[expected_id]
485 c.pending_responses.delete(expected_id)
486 return response
487 }
488 for {
489 raw_message := c.transport.receive()!
490 envelope := decode_envelope(raw_message)!
491 if envelope.method.len != 0 {
492 if is_notification_id(envelope.id) {
493 c.notifications << Notification{
494 method: envelope.method
495 params: envelope.params
496 }
497 } else {
498 c.server_requests << Request{
499 id: envelope.id
500 method: envelope.method
501 params: envelope.params
502 }
503 }
504 continue
505 }
506 response := Response{
507 id: envelope.id
508 result: envelope.result
509 error: envelope.error
510 }
511 if response.id == expected_id {
512 return response
513 }
514 c.pending_responses[response.id] = response
515 }
516 return error('mcp: response loop exited unexpectedly')
517}
518
519fn encode_initialize_params(params InitializeParams) string {
520 return '{"protocolVersion":${json.encode(params.protocol_version)},"capabilities":${normalize_capabilities(params.capabilities)},"clientInfo":${json.encode(params.client_info)}}'
521}
522
523struct NoFrameError {}
524
525fn (err NoFrameError) msg() string {
526 return 'no complete frame available'
527}
528
529fn (err NoFrameError) code() int {
530 return 0
531}
532
533// FrameExtraction holds a single stdio message and the unconsumed buffer remainder.
534struct FrameExtraction {
535 message string
536 remaining string
537}
538
539struct HttpTransport {
540mut:
541 url string
542 header http.Header
543 session_id string
544 protocol_version string
545 pending []string
546}
547
548fn new_http_transport(url string, config ClientConfig) !HttpTransport {
549 if url == '' {
550 return error('mcp.connect_http: empty url')
551 }
552 if !url.starts_with('http://') && !url.starts_with('https://') {
553 return error('mcp.connect_http: expected an http:// or https:// MCP endpoint')
554 }
555 mut header := http.new_header()
556 header.set(.user_agent, default_client_name)
557 if config.headers.len != 0 {
558 header.add_custom_map(config.headers)!
559 }
560 return HttpTransport{
561 url: url
562 header: header
563 }
564}
565
566fn (mut transport HttpTransport) send(message string) ! {
567 mut header := transport.header
568 header.set(.content_type, default_content_type)
569 header.set(.accept, streamable_http_accept)
570 if transport.session_id != '' {
571 header.set_custom(mcp_session_id_header, transport.session_id)!
572 }
573 if transport.protocol_version != '' {
574 header.set_custom(mcp_protocol_version_header, transport.protocol_version)!
575 }
576 response := http.fetch(
577 method: .post
578 url: transport.url
579 data: message
580 header: header
581 )!
582 if session_id := response.header.get_custom(mcp_session_id_header) {
583 transport.session_id = session_id
584 }
585 if transport.protocol_version == '' && transport.session_id != '' {
586 // First handshake response: capture the negotiated version so all
587 // subsequent requests carry MCP-Protocol-Version per spec §Transports.
588 transport.protocol_version = read_negotiated_version(response.body)
589 }
590 messages := parse_http_response_messages(response)!
591 if messages.len != 0 {
592 transport.pending << messages
593 return
594 }
595 if response.status_code >= 400 {
596 return error('mcp.http: server returned HTTP ${response.status_code} without an MCP payload')
597 }
598}
599
600fn read_negotiated_version(body string) string {
601 envelope := decode_envelope(body) or { return '' }
602 result := json.decode(InitializeResult, envelope.result) or { return '' }
603 return result.protocol_version
604}
605
606fn (mut transport HttpTransport) receive() !string {
607 if transport.pending.len == 0 {
608 return error('mcp.http: no pending messages are available')
609 }
610 message := transport.pending[0]
611 transport.pending = if transport.pending.len == 1 {
612 []string{}
613 } else {
614 transport.pending[1..].clone()
615 }
616 return message
617}
618
619fn (mut transport HttpTransport) close() {
620 if transport.session_id == '' {
621 return
622 }
623 mut header := transport.header
624 header.set_custom(mcp_session_id_header, transport.session_id) or { return }
625 http.fetch(
626 method: .delete
627 url: transport.url
628 header: header
629 ) or {}
630 transport.session_id = ''
631}
632
633struct ProcessTransport {
634mut:
635 process &os.Process
636 buffer string
637}
638
639fn new_process_transport(command string, args []string) !ProcessTransport {
640 if command == '' {
641 return error('mcp.connect_stdio: empty command')
642 }
643 mut process := os.new_process(command)
644 process.set_args(args)
645 process.set_redirect_stdio()
646 process.run()
647 return ProcessTransport{
648 process: process
649 }
650}
651
652fn (mut transport ProcessTransport) send(message string) ! {
653 transport.process.stdin_write(encode_stdio_message(message))
654}
655
656fn (mut transport ProcessTransport) receive() !string {
657 for {
658 frame := try_extract_stdio_message(transport.buffer) or {
659 if err.msg() != NoFrameError{}.msg() {
660 return err
661 }
662 FrameExtraction{}
663 }
664 if frame.message.len != 0 {
665 transport.buffer = frame.remaining
666 return frame.message
667 }
668 if transport.process.is_pending(.stdout) {
669 chunk := transport.process.stdout_read()
670 if chunk.len != 0 {
671 transport.buffer += chunk
672 continue
673 }
674 }
675 if !transport.process.is_alive() {
676 transport.buffer += transport.process.stdout_slurp()
677 frame_after_exit := try_extract_stdio_message(transport.buffer) or {
678 if err.msg() != NoFrameError{}.msg() {
679 return err
680 }
681 FrameExtraction{}
682 }
683 if frame_after_exit.message.len != 0 {
684 transport.buffer = frame_after_exit.remaining
685 return frame_after_exit.message
686 }
687 stderr_output := transport.process.stderr_slurp().trim_space()
688 if stderr_output.len != 0 {
689 return error('mcp.stdio: process exited before a full MCP message was received: ${stderr_output}')
690 }
691 return error('mcp.stdio: process exited before a full MCP message was received')
692 }
693 time.sleep(process_poll_interval)
694 }
695 return error('mcp.stdio: receive loop exited unexpectedly')
696}
697
698fn (mut transport ProcessTransport) close() {
699 if transport.process.is_alive() {
700 transport.process.signal_term()
701 for _ in 0 .. 20 {
702 if !transport.process.is_alive() {
703 break
704 }
705 time.sleep(10 * time.millisecond)
706 }
707 if transport.process.is_alive() {
708 transport.process.signal_kill()
709 } else if transport.process.status in [.running, .stopped] {
710 transport.process.wait()
711 }
712 }
713 transport.process.close()
714}
715
716fn parse_http_response_messages(response http.Response) ![]string {
717 content_type := response.header.get(.content_type) or { '' }
718 body := response.body.trim_space()
719 if body.len == 0 {
720 return []string{}
721 }
722 content_type_lower := content_type.to_lower()
723 if content_type_lower.starts_with('application/json')
724 || (content_type == '' && is_json_payload(body)) {
725 return split_json_payloads(body)
726 }
727 if content_type_lower.starts_with('text/event-stream') {
728 return parse_sse_messages(body)
729 }
730 return error('mcp.http: unsupported content type `${content_type}`')
731}
732
733fn split_json_payloads(body string) ![]string {
734 trimmed := body.trim_space()
735 if trimmed.len == 0 {
736 return []string{}
737 }
738 if trimmed[0] != `[` {
739 return [trimmed]
740 }
741 envelopes := json.decode([]MessageEnvelope, trimmed) or { return err }
742 mut messages := []string{cap: envelopes.len}
743 for envelope in envelopes {
744 messages << envelope.encode()
745 }
746 return messages
747}
748
749fn parse_sse_messages(body string) ![]string {
750 normalized := body.replace('\r\n', '\n').replace('\r', '\n')
751 mut data_lines := []string{}
752 mut messages := []string{}
753 for line in normalized.split('\n') {
754 if line.len == 0 {
755 if data_lines.len != 0 {
756 append_sse_payload(mut messages, data_lines.join('\n'))!
757 data_lines = []string{}
758 }
759 continue
760 }
761 if line.starts_with(':') {
762 continue
763 }
764 if line.starts_with('data:') {
765 mut payload := line[5..]
766 if payload.len != 0 && payload[0] == ` ` {
767 payload = payload[1..]
768 }
769 data_lines << payload
770 }
771 }
772 if data_lines.len != 0 {
773 append_sse_payload(mut messages, data_lines.join('\n'))!
774 }
775 return messages
776}
777
778fn append_sse_payload(mut messages []string, payload string) ! {
779 trimmed := payload.trim_space()
780 if !is_json_payload(trimmed) {
781 return
782 }
783 payloads := split_json_payloads(trimmed)!
784 for item in payloads {
785 messages << item
786 }
787}
788
789fn is_json_payload(payload string) bool {
790 trimmed := payload.trim_space()
791 if trimmed.len == 0 {
792 return false
793 }
794 return trimmed[0] == `{` || trimmed[0] == `[`
795}
796
797// encode_stdio_message produces a newline-delimited stdio frame per MCP spec.
798// The MCP spec mandates that messages are delimited by newlines and MUST NOT
799// contain embedded newlines. Compact JSON encoding satisfies the latter; we
800// strip stray CR/LF defensively to keep the on-wire contract.
801fn encode_stdio_message(message string) string {
802 return message.replace('\r', '').replace('\n', '') + '\n'
803}
804
805// try_extract_stdio_message consumes the next newline-delimited frame from
806// `buffer`, returning the message body without its trailing newline.
807fn try_extract_stdio_message(buffer string) !FrameExtraction {
808 newline := buffer.index('\n') or { return NoFrameError{} }
809 mut end := newline
810 if end > 0 && buffer[end - 1] == `\r` {
811 end--
812 }
813 message := buffer[..end].trim_space()
814 remaining := if newline + 1 >= buffer.len { '' } else { buffer[newline + 1..] }
815 if message.len == 0 {
816 return try_extract_stdio_message(remaining) or {
817 if err.msg() == NoFrameError{}.msg() {
818 return NoFrameError{}
819 }
820 err
821 }
822 }
823 return FrameExtraction{
824 message: message
825 remaining: remaining
826 }
827}
828
829fn encode_id[I](id I) string {
830 return $if I is int {
831 id.str()
832 } $else {
833 json.encode(id)
834 }
835}
836
837fn encode_value[T](value T) string {
838 return $if T is Empty {
839 value.str()
840 } $else $if T is EmptyObject {
841 value.str()
842 } $else $if T is Null {
843 value.str()
844 } $else {
845 json.encode(value)
846 }
847}
848
849fn decode_value[T](value string) !T {
850 $if T is Empty {
851 if value == '' || value == null.str() {
852 return Empty{}
853 }
854 return error('mcp: expected an empty payload, got `${value}`')
855 } $else $if T is EmptyObject {
856 if value == '{}' {
857 return EmptyObject{}
858 }
859 return error('mcp: expected an empty object payload, got `${value}`')
860 } $else $if T is Null {
861 if value == null.str() {
862 return null
863 }
864 return error('mcp: expected null, got `${value}`')
865 } $else $if T is string {
866 if value.len >= 2 && value[0] == `"` && value[value.len - 1] == `"` {
867 return json.decode(string, value) or { return err }
868 }
869 return error('mcp: could not decode `${value}` into string')
870 } $else $if T is bool {
871 if value == 'true' {
872 return true
873 }
874 if value == 'false' {
875 return false
876 }
877 return error('mcp: could not decode `${value}` into bool')
878 } $else {
879 return json.decode(T, value) or { return err }
880 }
881}
882
883fn normalize_client_info(client_info Implementation) Implementation {
884 return if client_info.name == '' {
885 Implementation{
886 name: default_client_name
887 version: if client_info.version == '' {
888 default_client_version
889 } else {
890 client_info.version
891 }
892 }
893 } else if client_info.version == '' {
894 Implementation{
895 name: client_info.name
896 version: default_client_version
897 }
898 } else {
899 client_info
900 }
901}
902
903fn normalize_capabilities(capabilities string) string {
904 trimmed := capabilities.trim_space()
905 return if trimmed.len == 0 { '{}' } else { trimmed }
906}
907
908fn normalize_protocol_version(version string) string {
909 trimmed := version.trim_space()
910 return if trimmed.len == 0 { protocol_version } else { trimmed }
911}
912