v2 / vlib / mcp / server.v
1157 lines · 1058 sloc · 30.54 KB · 26926f194fd79184bc6758f8ece86e5c4a6ab44d
Raw
1module mcp
2
3import json
4import io
5import net.http
6import os
7import rand
8import time
9
10const default_server_name = 'v.mcp.server'
11const default_server_version = 'dev'
12const default_http_path = '/mcp'
13const default_list_page_size = 50
14const stdio_session_id = 'stdio'
15const event_stream_content_type = 'text/event-stream'
16
17// SessionTransport identifies how an MCP session is connected.
18pub enum SessionTransport {
19 stdio
20 http
21}
22
23// Context provides request-scoped server metadata to handlers.
24pub struct Context {
25pub:
26 session_id string @[json: sessionId]
27 request_id string @[json: requestId]
28 method string
29 transport SessionTransport
30 protocol_version string @[json: protocolVersion]
31 client_info Implementation @[json: clientInfo]
32 client_capabilities string @[json: clientCapabilities; raw]
33}
34
35// Tool describes an MCP tool exposed by the server.
36pub struct Tool {
37pub:
38 name string
39 title string @[omitempty]
40 description string @[omitempty]
41 input_schema string = default_tool_input_schema @[json: inputSchema; raw]
42 output_schema string @[json: outputSchema; omitempty; raw]
43}
44
45// Resource describes a concrete MCP resource exposed by the server.
46pub struct Resource {
47pub:
48 uri string
49 name string
50 title string @[omitempty]
51 description string @[omitempty]
52 mime_type string @[json: mimeType; omitempty]
53}
54
55// ResourceTemplate describes a parameterized MCP resource URI template.
56pub struct ResourceTemplate {
57pub:
58 uri_template string @[json: uriTemplate]
59 name string
60 title string @[omitempty]
61 description string @[omitempty]
62 mime_type string @[json: mimeType; omitempty]
63}
64
65// ResourceContents contains the result of `resources/read`.
66pub struct ResourceContents {
67pub:
68 uri string
69 mime_type string @[json: mimeType; omitempty]
70 text string @[omitempty]
71 blob string @[omitempty]
72}
73
74// ReadResourceResult is returned by `resources/read`.
75pub struct ReadResourceResult {
76pub:
77 contents []ResourceContents
78}
79
80// PromptArgument describes one prompt argument.
81pub struct PromptArgument {
82pub:
83 name string
84 description string @[omitempty]
85 required bool
86}
87
88// Prompt describes an MCP prompt exposed by the server.
89pub struct Prompt {
90pub:
91 name string
92 title string @[omitempty]
93 description string @[omitempty]
94 arguments []PromptArgument
95}
96
97// PromptMessage is one message returned by `prompts/get`.
98pub struct PromptMessage {
99pub:
100 role string
101 content string @[raw]
102}
103
104// GetPromptResult is returned by `prompts/get`.
105pub struct GetPromptResult {
106pub:
107 description string @[omitempty]
108 messages []PromptMessage
109}
110
111// ToolResult is returned by `tools/call`.
112pub struct ToolResult {
113pub:
114 content string @[omitempty; raw]
115 structured_content string @[json: structuredContent; omitempty; raw]
116 is_error bool @[json: isError]
117}
118
119// ToolHandler handles `tools/call` for a registered tool.
120pub type ToolHandler = fn (ctx Context, arguments string) !ToolResult
121
122// ResourceHandler handles `resources/read` for a registered resource URI.
123pub type ResourceHandler = fn (ctx Context, uri string) !ReadResourceResult
124
125// PromptHandler handles `prompts/get` for a registered prompt.
126pub type PromptHandler = fn (ctx Context, arguments string) !GetPromptResult
127
128// ServerConfig configures an MCP server instance.
129@[params]
130pub struct ServerConfig {
131pub:
132 name string
133 version string
134 protocol_version string = protocol_version
135 capabilities string
136 instructions string
137 http_path string = default_http_path
138}
139
140struct RegisteredTool {
141 tool Tool
142 handler ToolHandler = unsafe { nil }
143}
144
145struct RegisteredResource {
146 resource Resource
147 handler ResourceHandler = unsafe { nil }
148}
149
150struct RegisteredPrompt {
151 prompt Prompt
152 handler PromptHandler = unsafe { nil }
153}
154
155struct Session {
156mut:
157 id string
158 transport SessionTransport
159 protocol_version string
160 client_info Implementation
161 client_capabilities string
162 initialize_complete bool
163 initialized bool
164}
165
166struct ServerState {
167mut:
168 sessions map[string]Session
169}
170
171struct DispatchResult {
172 has_response bool
173 response string
174 session_id string
175}
176
177struct HandledRequest {
178 response Response
179 session_id string
180}
181
182struct ProtocolError {
183 response_error ResponseError
184 request_id string
185}
186
187fn (err ProtocolError) msg() string {
188 return err.response_error.message
189}
190
191fn (err ProtocolError) code() int {
192 return err.response_error.code
193}
194
195struct CursorParams {
196 cursor string
197}
198
199struct ToolCallParams {
200 name string
201 arguments string @[raw]
202}
203
204struct ReadResourceParams {
205 uri string
206}
207
208struct GetPromptParams {
209 name string
210 arguments string @[raw]
211}
212
213struct ListToolsResult {
214 tools []Tool
215 next_cursor string @[json: nextCursor; omitempty]
216}
217
218struct ListResourcesResult {
219 resources []Resource
220 next_cursor string @[json: nextCursor; omitempty]
221}
222
223struct ListResourceTemplatesResult {
224 resource_templates []ResourceTemplate @[json: resourceTemplates]
225 next_cursor string @[json: nextCursor; omitempty]
226}
227
228struct ListPromptsResult {
229 prompts []Prompt
230 next_cursor string @[json: nextCursor; omitempty]
231}
232
233const default_tool_input_schema = '{"type":"object","additionalProperties":false}'
234
235// Server handles MCP protocol requests for stdio and HTTP transports.
236@[heap]
237pub struct Server {
238mut:
239 server_info Implementation
240 protocol_version string
241 capabilities_override string
242 instructions string
243 http_path string
244 http_server &http.Server = unsafe { nil }
245 tools map[string]RegisteredTool
246 tool_names []string
247 resources map[string]RegisteredResource
248 resource_uris []string
249 resource_templates map[string]ResourceTemplate
250 resource_template_ids []string
251 prompts map[string]RegisteredPrompt
252 prompt_names []string
253 state shared ServerState
254}
255
256// new_server constructs a new MCP server.
257pub fn new_server(config ServerConfig) Server {
258 return Server{
259 server_info: normalize_server_info(config.name, config.version)
260 protocol_version: normalize_protocol_version(config.protocol_version)
261 capabilities_override: config.capabilities.trim_space()
262 instructions: config.instructions
263 http_path: normalize_http_path(config.http_path)
264 tools: map[string]RegisteredTool{}
265 resources: map[string]RegisteredResource{}
266 resource_templates: map[string]ResourceTemplate{}
267 prompts: map[string]RegisteredPrompt{}
268 state: ServerState{}
269 }
270}
271
272// add_tool registers a tool and its handler.
273pub fn (mut s Server) add_tool(tool Tool, handler ToolHandler) ! {
274 validate_tool_name(tool.name)!
275 if tool.name in s.tools {
276 return error('mcp.Server.add_tool: duplicate tool `${tool.name}`')
277 }
278 normalized := Tool{
279 name: tool.name
280 title: tool.title
281 description: tool.description
282 input_schema: normalize_tool_input_schema(tool.input_schema)
283 output_schema: tool.output_schema.trim_space()
284 }
285 s.tools[tool.name] = RegisteredTool{
286 tool: normalized
287 handler: handler
288 }
289 s.tool_names << tool.name
290}
291
292// add_resource registers a concrete resource and its read handler.
293pub fn (mut s Server) add_resource(resource Resource, handler ResourceHandler) ! {
294 if resource.uri.trim_space() == '' {
295 return error('mcp.Server.add_resource: empty uri')
296 }
297 if resource.uri in s.resources {
298 return error('mcp.Server.add_resource: duplicate resource `${resource.uri}`')
299 }
300 s.resources[resource.uri] = RegisteredResource{
301 resource: resource
302 handler: handler
303 }
304 s.resource_uris << resource.uri
305}
306
307// add_resource_template registers a resource template exposed by `resources/templates/list`.
308pub fn (mut s Server) add_resource_template(template ResourceTemplate) ! {
309 if template.uri_template.trim_space() == '' {
310 return error('mcp.Server.add_resource_template: empty uri template')
311 }
312 if template.uri_template in s.resource_templates {
313 return error('mcp.Server.add_resource_template: duplicate resource template `${template.uri_template}`')
314 }
315 s.resource_templates[template.uri_template] = template
316 s.resource_template_ids << template.uri_template
317}
318
319// add_prompt registers a prompt and its handler.
320pub fn (mut s Server) add_prompt(prompt Prompt, handler PromptHandler) ! {
321 if prompt.name.trim_space() == '' {
322 return error('mcp.Server.add_prompt: empty prompt name')
323 }
324 if prompt.name in s.prompts {
325 return error('mcp.Server.add_prompt: duplicate prompt `${prompt.name}`')
326 }
327 s.prompts[prompt.name] = RegisteredPrompt{
328 prompt: prompt
329 handler: handler
330 }
331 s.prompt_names << prompt.name
332}
333
334// serve_stdio starts serving MCP messages over stdio using Content-Length framing.
335pub fn (mut s Server) serve_stdio() ! {
336 mut stdin := os.stdin()
337 mut stdout := os.stdout()
338 s.serve_framed_transport(mut stdin, mut stdout, stdio_session_id, .stdio)!
339}
340
341// serve_http starts serving MCP over a single HTTP endpoint.
342pub fn (mut s Server) serve_http(addr string) ! {
343 if addr.trim_space() == '' {
344 return error('mcp.Server.serve_http: empty address')
345 }
346 mut handler := HttpHandler{
347 server: s
348 }
349 mut http_server := &http.Server{
350 addr: addr
351 handler: handler
352 accept_timeout: 100 * time.millisecond
353 show_startup_message: false
354 }
355 s.http_server = http_server
356 http_server.listen_and_serve()
357}
358
359// close stops the HTTP server if it is running.
360pub fn (mut s Server) close() {
361 if !isnil(s.http_server) {
362 s.http_server.close()
363 }
364}
365
366// wait_till_running waits until the HTTP server transitions to the running state.
367pub fn (mut s Server) wait_till_running(params http.WaitTillRunningParams) !int {
368 mut retries := 0
369 for isnil(s.http_server) && retries < params.max_retries {
370 time.sleep(params.retry_period_ms * time.millisecond)
371 retries++
372 }
373 if isnil(s.http_server) {
374 return error('mcp.Server.wait_till_running: HTTP server is not running')
375 }
376 remaining_retries := if params.max_retries > retries { params.max_retries - retries } else { 0 }
377 if remaining_retries == 0 {
378 return error('mcp.Server.wait_till_running: HTTP server is not running')
379 }
380 retry_count := s.http_server.wait_till_running(
381 max_retries: remaining_retries
382 retry_period_ms: params.retry_period_ms
383 )!
384 return retries + retry_count
385}
386
387// text_content creates a raw MCP text content item.
388pub fn text_content(text string) string {
389 return '{"type":"text","text":${json.encode(text)}}'
390}
391
392// prompt_text_message creates a prompt message with text content.
393pub fn prompt_text_message(role string, text string) PromptMessage {
394 return PromptMessage{
395 role: role
396 content: text_content(text)
397 }
398}
399
400// tool_text_result wraps plain text in an MCP tool result.
401pub fn tool_text_result(text string) ToolResult {
402 return ToolResult{
403 content: '[${text_content(text)}]'
404 }
405}
406
407fn tool_error_result(text string) ToolResult {
408 return ToolResult{
409 content: '[${text_content(text)}]'
410 is_error: true
411 }
412}
413
414fn response_with_json(request_id string, result_json string) Response {
415 return Response{
416 id: request_id
417 result: result_json
418 }
419}
420
421fn encode_initialize_result(result InitializeResult) string {
422 mut fields := [
423 '"protocolVersion":${json.encode(result.protocol_version)}',
424 '"capabilities":${normalize_capabilities(result.capabilities)}',
425 '"serverInfo":${json.encode(result.server_info)}',
426 ]
427 if result.instructions != '' {
428 fields << '"instructions":${json.encode(result.instructions)}'
429 }
430 return '{${fields.join(',')}}'
431}
432
433fn encode_tools_list_result(result ListToolsResult) string {
434 mut fields := ['"tools":[${result.tools.map(encode_tool).join(',')}]']
435 if result.next_cursor != '' {
436 fields << '"nextCursor":${json.encode(result.next_cursor)}'
437 }
438 return '{${fields.join(',')}}'
439}
440
441fn encode_tool(tool Tool) string {
442 mut fields := ['"name":${json.encode(tool.name)}',
443 '"inputSchema":${normalize_tool_input_schema(tool.input_schema)}']
444 if tool.title != '' {
445 fields << '"title":${json.encode(tool.title)}'
446 }
447 if tool.description != '' {
448 fields << '"description":${json.encode(tool.description)}'
449 }
450 if tool.output_schema.trim_space() != '' {
451 fields << '"outputSchema":${tool.output_schema.trim_space()}'
452 }
453 return '{${fields.join(',')}}'
454}
455
456fn encode_tool_result(result ToolResult) string {
457 mut fields := ['"isError":${result.is_error.str()}']
458 if result.content.trim_space() != '' {
459 fields << '"content":${result.content.trim_space()}'
460 }
461 if result.structured_content.trim_space() != '' {
462 fields << '"structuredContent":${result.structured_content.trim_space()}'
463 }
464 return '{${fields.join(',')}}'
465}
466
467fn encode_prompt_result(result GetPromptResult) string {
468 mut fields := [
469 '"messages":[${result.messages.map(encode_prompt_message).join(',')}]',
470 ]
471 if result.description != '' {
472 fields << '"description":${json.encode(result.description)}'
473 }
474 return '{${fields.join(',')}}'
475}
476
477fn encode_prompt_message(message PromptMessage) string {
478 return '{"role":${json.encode(message.role)},"content":${message.content}}'
479}
480
481fn (mut s Server) serve_framed_transport(mut reader io.Reader, mut writer io.Writer, session_id string, transport SessionTransport) ! {
482 mut buffer := ''
483 for {
484 frame := try_extract_framed_message(buffer) or {
485 if err.msg() != NoFrameError{}.msg() {
486 error_response := Response{
487 error: normalize_response_error(err)
488 }.encode()
489 writer.write(encode_framed_message(error_response).bytes())!
490 return err
491 }
492 FrameExtraction{}
493 }
494 if frame.message.len != 0 {
495 buffer = frame.remaining
496 dispatch_result := s.dispatch_message(frame.message, session_id, transport) or {
497 error_response := Response{
498 error: normalize_response_error(err)
499 }.encode()
500 writer.write(encode_framed_message(error_response).bytes())!
501 continue
502 }
503 if dispatch_result.has_response {
504 writer.write(encode_framed_message(dispatch_result.response).bytes())!
505 }
506 continue
507 }
508 mut chunk := []u8{len: 4096}
509 bytes_read := reader.read(mut chunk) or {
510 if err is os.Eof {
511 return
512 }
513 if err is io.Eof {
514 return
515 }
516 return err
517 }
518 if bytes_read == 0 {
519 return
520 }
521 buffer += chunk[..bytes_read].bytestr()
522 }
523}
524
525fn (mut s Server) dispatch_message(raw string, session_id string, transport SessionTransport) !DispatchResult {
526 trimmed := raw.trim_space()
527 if trimmed.len == 0 || trimmed[0] == `[` {
528 return ProtocolError{
529 response_error: invalid_request
530 }
531 }
532 envelope := decode_envelope(trimmed) or {
533 return ProtocolError{
534 response_error: parse_error
535 }
536 }
537 return s.dispatch_envelope(envelope, session_id, transport)
538}
539
540fn (mut s Server) dispatch_envelope(envelope MessageEnvelope, session_id string, transport SessionTransport) !DispatchResult {
541 if envelope.method.len == 0 {
542 return DispatchResult{}
543 }
544 if envelope.id.len == 0 || envelope.id == null.str() {
545 s.handle_notification(Notification{
546 method: envelope.method
547 params: envelope.params
548 }, session_id, transport)!
549 return DispatchResult{}
550 }
551 req := Request{
552 id: envelope.id
553 method: envelope.method
554 params: envelope.params
555 }
556 handled := s.handle_request(req, session_id, transport)
557 return DispatchResult{
558 has_response: true
559 response: handled.response.encode()
560 session_id: handled.session_id
561 }
562}
563
564fn (mut s Server) handle_request(req Request, session_id string, transport SessionTransport) HandledRequest {
565 return s.handle_request_impl(req, session_id, transport) or {
566 return HandledRequest{
567 response: error_response_for(req.id, err)
568 }
569 }
570}
571
572fn (mut s Server) handle_request_impl(req Request, session_id string, transport SessionTransport) !HandledRequest {
573 if req.method == 'ping' {
574 return HandledRequest{
575 response: response_with_json(req.id, empty_object.str())
576 }
577 }
578 if req.method == 'initialize' {
579 return s.handle_initialize(req, session_id, transport)
580 }
581 session := s.session_for_request(session_id, transport) or {
582 return ProtocolError{
583 response_error: server_not_initialized
584 request_id: req.id
585 }
586 }
587 if !session.initialize_complete || !session.initialized {
588 return ProtocolError{
589 response_error: server_not_initialized
590 request_id: req.id
591 }
592 }
593 ctx := s.context_from_session(req, session)
594 match req.method {
595 'tools/list' {
596 return s.handle_tools_list(req)
597 }
598 'tools/call' {
599 return s.handle_tools_call(req, ctx)
600 }
601 'resources/list' {
602 return s.handle_resources_list(req)
603 }
604 'resources/read' {
605 return s.handle_resources_read(req, ctx)
606 }
607 'resources/templates/list' {
608 return s.handle_resource_templates_list(req)
609 }
610 'prompts/list' {
611 return s.handle_prompts_list(req)
612 }
613 'prompts/get' {
614 return s.handle_prompts_get(req, ctx)
615 }
616 else {
617 return ProtocolError{
618 response_error: method_not_found
619 request_id: req.id
620 }
621 }
622 }
623
624 return ProtocolError{
625 response_error: method_not_found
626 request_id: req.id
627 }
628}
629
630fn (mut s Server) handle_initialize(req Request, session_id string, transport SessionTransport) !HandledRequest {
631 params := req.decode_params[InitializeParams]() or {
632 return ProtocolError{
633 response_error: invalid_params
634 request_id: req.id
635 }
636 }
637 mut session := s.ensure_session_for_initialize(session_id, transport)
638 if session.initialize_complete {
639 return ProtocolError{
640 response_error: invalid_request
641 request_id: req.id
642 }
643 }
644 session.protocol_version = if params.protocol_version == s.protocol_version {
645 s.protocol_version
646 } else {
647 s.protocol_version
648 }
649 session.client_info = normalize_client_info(params.client_info)
650 session.client_capabilities = normalize_capabilities(params.capabilities)
651 session.initialize_complete = true
652 session.initialized = false
653 s.store_session(session)
654 result := InitializeResult{
655 protocol_version: session.protocol_version
656 capabilities: s.capabilities_json()
657 server_info: s.server_info
658 instructions: s.instructions
659 }
660 return HandledRequest{
661 response: response_with_json(req.id, encode_initialize_result(result))
662 session_id: if transport == .http { session.id } else { '' }
663 }
664}
665
666fn (mut s Server) handle_tools_list(req Request) !HandledRequest {
667 params := decode_optional_params[CursorParams](req.params) or {
668 return ProtocolError{
669 response_error: invalid_params
670 request_id: req.id
671 }
672 }
673 start, end, next_cursor := paginate_bounds(s.tool_names.len, params.cursor)!
674 mut tools := []Tool{cap: end - start}
675 for name in s.tool_names[start..end] {
676 tools << s.tools[name].tool
677 }
678 return HandledRequest{
679 response: response_with_json(req.id, encode_tools_list_result(ListToolsResult{
680 tools: tools
681 next_cursor: next_cursor
682 }))
683 }
684}
685
686fn (mut s Server) handle_tools_call(req Request, ctx Context) !HandledRequest {
687 params := decode_optional_params[ToolCallParams](req.params) or {
688 return ProtocolError{
689 response_error: invalid_params
690 request_id: req.id
691 }
692 }
693 if params.name !in s.tools {
694 return ProtocolError{
695 response_error: invalid_params
696 request_id: req.id
697 }
698 }
699 entry := s.tools[params.name]
700 result := entry.handler(ctx, json_object_or_empty(params.arguments)) or {
701 if err is ProtocolError {
702 return err
703 }
704 if err is ResponseError {
705 return err
706 }
707 return HandledRequest{
708 response: response_with_json(req.id, encode_tool_result(tool_error_result(err.msg())))
709 }
710 }
711 return HandledRequest{
712 response: response_with_json(req.id, encode_tool_result(result))
713 }
714}
715
716fn (mut s Server) handle_resources_list(req Request) !HandledRequest {
717 params := decode_optional_params[CursorParams](req.params) or {
718 return ProtocolError{
719 response_error: invalid_params
720 request_id: req.id
721 }
722 }
723 start, end, next_cursor := paginate_bounds(s.resource_uris.len, params.cursor)!
724 mut resources := []Resource{cap: end - start}
725 for uri in s.resource_uris[start..end] {
726 resources << s.resources[uri].resource
727 }
728 return HandledRequest{
729 response: response_with_json(req.id, encode_value(ListResourcesResult{
730 resources: resources
731 next_cursor: next_cursor
732 }))
733 }
734}
735
736fn (mut s Server) handle_resources_read(req Request, ctx Context) !HandledRequest {
737 params := req.decode_params[ReadResourceParams]() or {
738 return ProtocolError{
739 response_error: invalid_params
740 request_id: req.id
741 }
742 }
743 if params.uri !in s.resources {
744 return ProtocolError{
745 response_error: invalid_params
746 request_id: req.id
747 }
748 }
749 entry := s.resources[params.uri]
750 result := entry.handler(ctx, params.uri) or { return err }
751 return HandledRequest{
752 response: response_with_json(req.id, encode_value(result))
753 }
754}
755
756fn (mut s Server) handle_resource_templates_list(req Request) !HandledRequest {
757 params := decode_optional_params[CursorParams](req.params) or {
758 return ProtocolError{
759 response_error: invalid_params
760 request_id: req.id
761 }
762 }
763 start, end, next_cursor := paginate_bounds(s.resource_template_ids.len, params.cursor)!
764 mut templates := []ResourceTemplate{cap: end - start}
765 for uri_template in s.resource_template_ids[start..end] {
766 templates << s.resource_templates[uri_template]
767 }
768 return HandledRequest{
769 response: response_with_json(req.id, encode_value(ListResourceTemplatesResult{
770 resource_templates: templates
771 next_cursor: next_cursor
772 }))
773 }
774}
775
776fn (mut s Server) handle_prompts_list(req Request) !HandledRequest {
777 params := decode_optional_params[CursorParams](req.params) or {
778 return ProtocolError{
779 response_error: invalid_params
780 request_id: req.id
781 }
782 }
783 start, end, next_cursor := paginate_bounds(s.prompt_names.len, params.cursor)!
784 mut prompts := []Prompt{cap: end - start}
785 for name in s.prompt_names[start..end] {
786 prompts << s.prompts[name].prompt
787 }
788 return HandledRequest{
789 response: response_with_json(req.id, encode_value(ListPromptsResult{
790 prompts: prompts
791 next_cursor: next_cursor
792 }))
793 }
794}
795
796fn (mut s Server) handle_prompts_get(req Request, ctx Context) !HandledRequest {
797 params := decode_optional_params[GetPromptParams](req.params) or {
798 return ProtocolError{
799 response_error: invalid_params
800 request_id: req.id
801 }
802 }
803 if params.name !in s.prompts {
804 return ProtocolError{
805 response_error: invalid_params
806 request_id: req.id
807 }
808 }
809 entry := s.prompts[params.name]
810 result := entry.handler(ctx, json_object_or_empty(params.arguments)) or { return err }
811 return HandledRequest{
812 response: response_with_json(req.id, encode_prompt_result(result))
813 }
814}
815
816fn (mut s Server) handle_notification(notification Notification, session_id string, transport SessionTransport) ! {
817 match notification.method {
818 'notifications/initialized' {
819 mut session := s.session_for_request(session_id, transport) or {
820 return ProtocolError{
821 response_error: server_not_initialized
822 }
823 }
824 if !session.initialize_complete {
825 return ProtocolError{
826 response_error: server_not_initialized
827 }
828 }
829 session.initialized = true
830 s.store_session(session)
831 }
832 'notifications/cancelled' {}
833 else {}
834 }
835}
836
837fn (s &Server) capabilities_json() string {
838 if s.capabilities_override != '' {
839 return normalize_capabilities(s.capabilities_override)
840 }
841 mut parts := []string{}
842 if s.tool_names.len != 0 {
843 parts << '"tools":{}'
844 }
845 if s.resource_uris.len != 0 || s.resource_template_ids.len != 0 {
846 parts << '"resources":{}'
847 }
848 if s.prompt_names.len != 0 {
849 parts << '"prompts":{}'
850 }
851 return '{${parts.join(',')}}'
852}
853
854fn (mut s Server) ensure_session_for_initialize(session_id string, transport SessionTransport) Session {
855 if transport == .stdio {
856 return s.ensure_session(stdio_session_id, .stdio)
857 }
858 if session_id != '' {
859 return s.ensure_session(session_id, .http)
860 }
861 return s.create_http_session()
862}
863
864fn (mut s Server) ensure_session(session_id string, transport SessionTransport) Session {
865 if session := s.get_session(session_id) {
866 return session
867 }
868 session := Session{
869 id: session_id
870 transport: transport
871 protocol_version: s.protocol_version
872 }
873 s.store_session(session)
874 return session
875}
876
877fn (mut s Server) create_http_session() Session {
878 for {
879 session_id := rand.uuid_v7()
880 if !s.session_exists(session_id) {
881 session := Session{
882 id: session_id
883 transport: .http
884 protocol_version: s.protocol_version
885 }
886 s.store_session(session)
887 return session
888 }
889 }
890 return Session{}
891}
892
893fn (s &Server) session_for_request(session_id string, transport SessionTransport) ?Session {
894 if transport == .stdio {
895 return s.get_session(stdio_session_id)
896 }
897 if session_id == '' {
898 return none
899 }
900 return s.get_session(session_id)
901}
902
903fn (mut s Server) store_session(session Session) {
904 lock s.state {
905 s.state.sessions[session.id] = session
906 }
907}
908
909fn (s &Server) get_session(session_id string) ?Session {
910 mut session := Session{}
911 mut found := false
912 rlock s.state {
913 if session_id in s.state.sessions {
914 session = s.state.sessions[session_id]
915 found = true
916 }
917 }
918 if !found {
919 return none
920 }
921 return session
922}
923
924fn (s &Server) session_exists(session_id string) bool {
925 mut found := false
926 rlock s.state {
927 found = session_id in s.state.sessions
928 }
929 return found
930}
931
932fn (mut s Server) delete_session(session_id string) bool {
933 mut deleted := false
934 lock s.state {
935 if session_id in s.state.sessions {
936 s.state.sessions.delete(session_id)
937 deleted = true
938 }
939 }
940 return deleted
941}
942
943fn (s &Server) context_from_session(req Request, session Session) Context {
944 return Context{
945 session_id: session.id
946 request_id: req.id
947 method: req.method
948 transport: session.transport
949 protocol_version: session.protocol_version
950 client_info: session.client_info
951 client_capabilities: session.client_capabilities
952 }
953}
954
955fn decode_optional_params[T](raw string) !T {
956 trimmed := raw.trim_space()
957 if trimmed.len == 0 || trimmed == null.str() {
958 return T{}
959 }
960 return decode_value[T](trimmed)
961}
962
963fn json_object_or_empty(raw string) string {
964 trimmed := raw.trim_space()
965 if trimmed.len == 0 || trimmed == null.str() {
966 return '{}'
967 }
968 return trimmed
969}
970
971fn paginate_bounds(total int, cursor string) !(int, int, string) {
972 mut start := 0
973 if cursor.trim_space() != '' {
974 start = cursor.int()
975 if start < 0 || start > total || (start == 0 && cursor != '0') {
976 return ProtocolError{
977 response_error: invalid_params
978 }
979 }
980 }
981 mut end := start + default_list_page_size
982 if end > total {
983 end = total
984 }
985 next_cursor := if end < total { end.str() } else { '' }
986 return start, end, next_cursor
987}
988
989fn validate_tool_name(name string) ! {
990 trimmed := name.trim_space()
991 if trimmed.len == 0 || trimmed.len > 128 {
992 return error('mcp.Server.add_tool: invalid tool name `${name}`')
993 }
994 for ch in trimmed {
995 if ch.is_letter() || ch.is_digit() || ch in [`_`, `-`, `.`] {
996 continue
997 }
998 return error('mcp.Server.add_tool: invalid tool name `${name}`')
999 }
1000}
1001
1002fn normalize_tool_input_schema(input_schema string) string {
1003 trimmed := input_schema.trim_space()
1004 return if trimmed.len == 0 { default_tool_input_schema } else { trimmed }
1005}
1006
1007fn normalize_server_info(name string, version string) Implementation {
1008 return Implementation{
1009 name: if name.trim_space() == '' { default_server_name } else { name.trim_space() }
1010 version: if version.trim_space() == '' {
1011 default_server_version
1012 } else {
1013 version.trim_space()
1014 }
1015 }
1016}
1017
1018fn normalize_http_path(path string) string {
1019 trimmed := path.trim_space()
1020 if trimmed == '' {
1021 return default_http_path
1022 }
1023 return if trimmed.starts_with('/') { trimmed } else { '/' + trimmed }
1024}
1025
1026fn error_response_for(request_id string, err IError) Response {
1027 mut response_id := request_id
1028 response_error := normalize_response_error(err)
1029 if err is ProtocolError && err.request_id != '' {
1030 response_id = err.request_id
1031 }
1032 return Response{
1033 id: response_id
1034 error: response_error
1035 }
1036}
1037
1038fn normalize_response_error(err IError) ResponseError {
1039 if err is ProtocolError {
1040 return err.response_error
1041 }
1042 if err is ResponseError {
1043 return err
1044 }
1045 return ResponseError{
1046 code: internal_error.code
1047 message: err.msg()
1048 }
1049}
1050
1051fn build_sse_response(message string) string {
1052 mut lines := []string{}
1053 for line in message.split('\n') {
1054 lines << 'data: ${line}'
1055 }
1056 return lines.join('\n') + '\n\n'
1057}
1058
1059fn accepts_event_stream_only(header http.Header) bool {
1060 accept := header.get(.accept) or { '' }
1061 if accept == '' {
1062 return false
1063 }
1064 accept_lower := accept.to_lower()
1065 return accept_lower.contains(event_stream_content_type)
1066 && !accept_lower.contains(default_content_type)
1067}
1068
1069fn json_http_response(status http.Status, body string, content_type string) http.Response {
1070 mut header := http.new_header()
1071 if content_type != '' {
1072 header.set(.content_type, content_type)
1073 }
1074 mut response := http.Response{
1075 body: body
1076 header: header
1077 }
1078 response.set_status(status)
1079 return response
1080}
1081
1082struct HttpHandler {
1083mut:
1084 server &Server = unsafe { nil }
1085}
1086
1087fn (mut h HttpHandler) handle(req http.Request) http.Response {
1088 return h.server.handle_http_request(req)
1089}
1090
1091fn (mut s Server) handle_http_request(req http.Request) http.Response {
1092 if req.url.all_before('?') != s.http_path {
1093 return json_http_response(.not_found, '', '')
1094 }
1095 session_id := req.header.get_custom(mcp_session_id_header) or { '' }
1096 match req.method {
1097 .delete {
1098 if session_id == '' {
1099 return json_http_response(.bad_request, '', '')
1100 }
1101 if !s.delete_session(session_id) {
1102 return json_http_response(.not_found, '', '')
1103 }
1104 return json_http_response(.ok, '', '')
1105 }
1106 .get {
1107 return json_http_response(.method_not_allowed, '', '')
1108 }
1109 .post {}
1110 else {
1111 return json_http_response(.method_not_allowed, '', '')
1112 }
1113 }
1114
1115 trimmed := req.data.trim_space()
1116 if trimmed.len == 0 || trimmed[0] == `[` {
1117 return json_http_response(.bad_request, Response{
1118 error: invalid_request
1119 }.encode(), default_content_type)
1120 }
1121 envelope := decode_envelope(trimmed) or {
1122 return json_http_response(.bad_request, Response{
1123 error: parse_error
1124 }.encode(), default_content_type)
1125 }
1126 if session_id != '' && !s.session_exists(session_id) {
1127 return json_http_response(.not_found, '', '')
1128 }
1129 if envelope.method != 'initialize' && envelope.method.len != 0 && session_id == '' {
1130 return json_http_response(.bad_request, Response{
1131 error: server_not_initialized
1132 }.encode(), default_content_type)
1133 }
1134 dispatch_result := s.dispatch_envelope(envelope, session_id, .http) or {
1135 return json_http_response(.bad_request, Response{
1136 error: normalize_response_error(err)
1137 }.encode(), default_content_type)
1138 }
1139 if !dispatch_result.has_response {
1140 return json_http_response(.accepted, '', '')
1141 }
1142 body := if accepts_event_stream_only(req.header) {
1143 build_sse_response(dispatch_result.response)
1144 } else {
1145 dispatch_result.response
1146 }
1147 content_type := if accepts_event_stream_only(req.header) {
1148 event_stream_content_type
1149 } else {
1150 default_content_type
1151 }
1152 mut response := json_http_response(.ok, body, content_type)
1153 if dispatch_result.session_id != '' {
1154 response.header.set_custom(mcp_session_id_header, dispatch_result.session_id) or {}
1155 }
1156 return response
1157}
1158