v / vlib / mcp / server.v
2451 lines · 2254 sloc · 71.2 KB · 7b55539b6e355cd5930ad6213fc49d3c884821dc
Raw
1module mcp
2
3import json
4import io
5import net.http
6import os
7import rand
8import sync
9import time
10
11const default_server_name = 'v.mcp.server'
12const default_server_version = 'dev'
13const default_http_path = '/mcp'
14const default_list_page_size = 50
15const completion_values_limit = 100
16const stdio_session_id = 'stdio'
17const event_log_capacity = 1024
18
19// SessionTransport identifies how an MCP session is connected.
20pub enum SessionTransport {
21 stdio
22 http
23}
24
25// LogLevel is the RFC 5424 severity used by `notifications/message`.
26pub enum LogLevel {
27 debug
28 info
29 notice
30 warning
31 error
32 critical
33 alert
34 emergency
35}
36
37// str returns the wire string for a log level (lowercase per spec).
38pub fn (l LogLevel) str() string {
39 return match l {
40 .debug { 'debug' }
41 .info { 'info' }
42 .notice { 'notice' }
43 .warning { 'warning' }
44 .error { 'error' }
45 .critical { 'critical' }
46 .alert { 'alert' }
47 .emergency { 'emergency' }
48 }
49}
50
51// parse_log_level decodes the wire string for a log level.
52pub fn parse_log_level(value string) ?LogLevel {
53 return match value {
54 'debug' { LogLevel.debug }
55 'info' { LogLevel.info }
56 'notice' { LogLevel.notice }
57 'warning' { LogLevel.warning }
58 'error' { LogLevel.error }
59 'critical' { LogLevel.critical }
60 'alert' { LogLevel.alert }
61 'emergency' { LogLevel.emergency }
62 else { none }
63 }
64}
65
66// Context provides request-scoped server metadata to handlers.
67pub struct Context {
68pub:
69 session_id string @[json: sessionId]
70 request_id string @[json: requestId]
71 method string
72 transport SessionTransport
73 protocol_version string @[json: protocolVersion]
74 client_info Implementation @[json: clientInfo]
75 client_capabilities string @[json: clientCapabilities; raw]
76 progress_token string @[json: progressToken; raw]
77mut:
78 server &Server = unsafe { nil } @[skip]
79}
80
81// is_cancelled reports whether the client has sent `notifications/cancelled`
82// for this request. Handlers should poll this for cooperative cancellation.
83pub fn (ctx Context) is_cancelled() bool {
84 if isnil(ctx.server) {
85 return false
86 }
87 return ctx.server.is_request_cancelled(ctx.session_id, ctx.request_id)
88}
89
90// notify_progress sends `notifications/progress` for this request when the
91// client included a `_meta.progressToken`. `total` and `message` are optional
92// (pass 0 / '' to omit).
93pub fn (ctx Context) notify_progress(progress f64, total f64, message string) {
94 if isnil(ctx.server) || ctx.progress_token.trim_space() == '' {
95 return
96 }
97 ctx.server.notify_progress_for(ctx.session_id, ctx.progress_token, progress, total, message)
98}
99
100// ToolAnnotations exposes the optional behavioural hints described by the
101// MCP spec (`tools/list` Annotations object).
102pub struct ToolAnnotations {
103pub:
104 title string @[omitempty]
105 read_only_hint ?bool @[json: readOnlyHint]
106 destructive_hint ?bool @[json: destructiveHint]
107 idempotent_hint ?bool @[json: idempotentHint]
108 open_world_hint ?bool @[json: openWorldHint]
109}
110
111// ToolExecution carries optional execution metadata for a tool. Set
112// `task_support` to one of `'forbidden'`, `'optional'` or `'required'` to
113// advertise tasks/* support; the default (empty) omits the field on the wire.
114pub struct ToolExecution {
115pub:
116 task_support string @[json: taskSupport; omitempty]
117}
118
119// Annotations are the optional rendering hints attached to resources, resource
120// templates and content blocks. `audience` is a list of MCP `Role` values
121// (e.g. `'user'`, `'assistant'`); `priority` is in the `[0.0, 1.0]` range with
122// higher meaning more important; `last_modified` is an ISO 8601 timestamp.
123pub struct Annotations {
124pub:
125 audience []string @[omitempty]
126 priority ?f64
127 last_modified string @[json: lastModified; omitempty]
128}
129
130// Tool describes an MCP tool exposed by the server.
131pub struct Tool {
132pub:
133 name string
134 title string @[omitempty]
135 description string @[omitempty]
136 input_schema string = default_tool_input_schema @[json: inputSchema; raw]
137 output_schema string @[json: outputSchema; omitempty; raw]
138 annotations ToolAnnotations
139 icons []Icon @[omitempty]
140 execution ToolExecution
141}
142
143// Resource describes a concrete MCP resource exposed by the server.
144pub struct Resource {
145pub:
146 uri string
147 name string
148 title string @[omitempty]
149 description string @[omitempty]
150 mime_type string @[json: mimeType; omitempty]
151 size ?int
152 icons []Icon @[omitempty]
153 annotations Annotations
154}
155
156// ResourceTemplate describes a parameterized MCP resource URI template.
157pub struct ResourceTemplate {
158pub:
159 uri_template string @[json: uriTemplate]
160 name string
161 title string @[omitempty]
162 description string @[omitempty]
163 mime_type string @[json: mimeType; omitempty]
164 icons []Icon @[omitempty]
165 annotations Annotations
166}
167
168// ResourceContents contains the result of `resources/read`.
169pub struct ResourceContents {
170pub:
171 uri string
172 mime_type string @[json: mimeType; omitempty]
173 text string @[omitempty]
174 blob string @[omitempty]
175}
176
177// ReadResourceResult is returned by `resources/read`.
178pub struct ReadResourceResult {
179pub:
180 contents []ResourceContents
181}
182
183// PromptArgument describes one prompt argument.
184pub struct PromptArgument {
185pub:
186 name string
187 description string @[omitempty]
188 required bool
189}
190
191// Prompt describes an MCP prompt exposed by the server.
192pub struct Prompt {
193pub:
194 name string
195 title string @[omitempty]
196 description string @[omitempty]
197 arguments []PromptArgument
198 icons []Icon @[omitempty]
199}
200
201// PromptMessage is one message returned by `prompts/get`.
202pub struct PromptMessage {
203pub:
204 role string
205 content string @[raw]
206}
207
208// GetPromptResult is returned by `prompts/get`.
209pub struct GetPromptResult {
210pub:
211 description string @[omitempty]
212 messages []PromptMessage
213}
214
215// ToolResult is returned by `tools/call`.
216pub struct ToolResult {
217pub:
218 content string @[omitempty; raw]
219 structured_content string @[json: structuredContent; omitempty; raw]
220 is_error bool @[json: isError]
221}
222
223// ToolHandler handles `tools/call` for a registered tool.
224pub type ToolHandler = fn (ctx Context, arguments string) !ToolResult
225
226// ResourceHandler handles `resources/read` for a registered resource URI.
227pub type ResourceHandler = fn (ctx Context, uri string) !ReadResourceResult
228
229// PromptHandler handles `prompts/get` for a registered prompt.
230pub type PromptHandler = fn (ctx Context, arguments string) !GetPromptResult
231
232// CompletionRef identifies the prompt or resource template a completion
233// targets. Prompt refs set `name`; resource refs set `uri`.
234pub struct CompletionRef {
235pub:
236 ref_type string @[json: type] // 'ref/prompt' or 'ref/resource'
237 name string @[omitempty]
238 uri string @[omitempty]
239}
240
241// CompletionResult is the payload returned by `completion/complete`.
242pub struct CompletionResult {
243pub:
244 values []string
245 total ?int
246 has_more ?bool
247}
248
249// CompletionHandler returns candidate values for a partial argument value.
250// `arguments_json` is the JSON object of already-supplied sibling arguments
251// (the spec's `context.arguments`).
252pub type CompletionHandler = fn (ctx Context, current_value string, arguments_json string) !CompletionResult
253
254// ServerConfig configures an MCP server instance.
255@[params]
256pub struct ServerConfig {
257pub:
258 name string
259 version string
260 title string
261 description string
262 website_url string
263 icons []Icon
264 protocol_version string = protocol_version
265 capabilities string
266 instructions string
267 http_path string = default_http_path
268 // enable_logging declares the `logging` capability and lets clients call
269 // `logging/setLevel`. Disabled by default — turn on when the server emits
270 // `notifications/message` payloads.
271 enable_logging bool
272 // allowed_origins lists the Origin header values accepted on Streamable
273 // HTTP. Use `*` to accept any origin (NOT recommended). When empty the
274 // server only accepts requests without an Origin header or from the
275 // loopback addresses (`http://localhost[:port]`, `http://127.0.0.1[:port]`,
276 // `http://[::1][:port]`, or the literal string `null`).
277 allowed_origins []string
278}
279
280struct RegisteredTool {
281 tool Tool
282 handler ToolHandler = unsafe { nil }
283}
284
285struct RegisteredResource {
286 resource Resource
287 handler ResourceHandler = unsafe { nil }
288}
289
290struct RegisteredPrompt {
291 prompt Prompt
292 handler PromptHandler = unsafe { nil }
293}
294
295struct RegisteredCompletion {
296 ref CompletionRef
297 argument string
298 handler CompletionHandler = unsafe { nil }
299}
300
301// Root identifies a filesystem-or-URI boundary advertised by the client in
302// response to `roots/list`.
303pub struct Root {
304pub:
305 uri string
306 name string @[omitempty]
307}
308
309// ListRootsResult is the typed payload returned by the client to a server
310// `roots/list` request.
311pub struct ListRootsResult {
312pub:
313 roots []Root
314}
315
316// SamplingMessage is one message of a sampling/createMessage exchange.
317pub struct SamplingMessage {
318pub:
319 role string
320 content string @[raw]
321}
322
323// ModelHint is a name hint for sampling/createMessage model selection.
324pub struct ModelHint {
325pub:
326 name string
327}
328
329// ModelPreferences expresses sampling/createMessage routing weights.
330pub struct ModelPreferences {
331pub:
332 hints []ModelHint
333 cost_priority f64 @[json: costPriority; omitempty]
334 speed_priority f64 @[json: speedPriority; omitempty]
335 intelligence_priority f64 @[json: intelligencePriority; omitempty]
336}
337
338// ToolChoice configures `sampling/createMessage` tool-use behaviour. `mode`
339// is one of `'auto'` (default), `'required'`, or `'none'`.
340pub struct ToolChoice {
341pub:
342 mode string
343}
344
345// CreateMessageParams are the typed parameters for sampling/createMessage.
346pub struct CreateMessageParams {
347pub:
348 messages []SamplingMessage
349 model_preferences ModelPreferences @[json: modelPreferences]
350 system_prompt string @[json: systemPrompt; omitempty]
351 max_tokens int @[json: maxTokens]
352 temperature f64 @[omitempty]
353 stop_sequences []string @[json: stopSequences; omitempty]
354 metadata string @[omitempty; raw]
355 tools []Tool @[omitempty]
356 tool_choice ToolChoice @[json: toolChoice; omitempty]
357 include_context string @[json: includeContext; omitempty]
358}
359
360// CreateMessageResult is the typed payload returned by the client.
361pub struct CreateMessageResult {
362pub:
363 role string
364 content string @[raw]
365 model string
366 stop_reason string @[json: stopReason]
367}
368
369// ElicitSchema is the requested object schema sent to the client for elicitation.
370pub struct ElicitSchema {
371pub:
372 type_ string = 'object' @[json: type]
373 properties string @[raw]
374 required []string @[omitempty]
375}
376
377// ElicitParams are the typed parameters for elicitation/create. Set `mode`
378// to `'url'` and supply `url` + `elicitation_id` to send a URL-mode request;
379// otherwise the call defaults to form mode and `requested_schema` is
380// expected to describe the form fields.
381pub struct ElicitParams {
382pub:
383 mode string @[omitempty]
384 message string
385 requested_schema ElicitSchema @[json: requestedSchema; omitempty]
386 url string @[omitempty]
387 elicitation_id string @[json: elicitationId; omitempty]
388}
389
390// ElicitResult is the typed payload returned by the client.
391pub struct ElicitResult {
392pub:
393 action string
394 content string @[omitempty; raw]
395}
396
397struct LoggedEvent {
398 id int
399 body string
400}
401
402struct Session {
403mut:
404 id string
405 transport SessionTransport
406 protocol_version string
407 client_info Implementation
408 client_capabilities string
409 initialize_complete bool
410 initialized bool
411 notification_queue []string
412 subscribed_uris []string
413 log_level LogLevel = .debug
414 log_level_set bool
415 cancelled_requests []string
416 event_log []LoggedEvent
417 next_event_id int = 1
418 next_request_seq int = 1
419 pending_responses map[string]Response
420 // progress_seen tracks the last progress value emitted for a given
421 // `progressToken`. The MCP spec requires `progress` values to be strictly
422 // monotonically increasing, so we silently drop any non-increasing call
423 // rather than send an out-of-order notification on the wire.
424 progress_seen map[string]f64
425}
426
427struct ServerState {
428mut:
429 sessions map[string]Session
430 // response_signals carries one semaphore per in-flight server-initiated
431 // request, keyed by `pending_signal_key(session_id, request_id)`.
432 // `wait_for_response` blocks on it (with a deadline) and
433 // `deliver_response` / `delete_session` post it so we never burn CPU
434 // polling for a response that may take seconds to arrive. Kept outside
435 // `Session` to avoid V's pointer-in-shared-map access warnings.
436 response_signals map[string]&sync.Semaphore
437}
438
439fn pending_signal_key(session_id string, request_id string) string {
440 return '${session_id}\x00${request_id}'
441}
442
443struct DispatchResult {
444 has_response bool
445 response string
446 session_id string
447}
448
449struct HandledRequest {
450 response Response
451 session_id string
452}
453
454struct ProtocolError {
455 response_error ResponseError
456 request_id string
457}
458
459fn (err ProtocolError) msg() string {
460 return err.response_error.message
461}
462
463fn (err ProtocolError) code() int {
464 return err.response_error.code
465}
466
467struct CursorParams {
468 cursor string
469}
470
471struct ToolCallParams {
472 name string
473 arguments string @[raw]
474}
475
476struct ReadResourceParams {
477 uri string
478}
479
480struct SubscribeParams {
481 uri string
482}
483
484struct GetPromptParams {
485 name string
486 arguments string @[raw]
487}
488
489struct ListToolsResult {
490 tools []Tool
491 next_cursor string @[json: nextCursor; omitempty]
492}
493
494struct ListResourcesResult {
495 resources []Resource
496 next_cursor string @[json: nextCursor; omitempty]
497}
498
499struct ListResourceTemplatesResult {
500 resource_templates []ResourceTemplate @[json: resourceTemplates]
501 next_cursor string @[json: nextCursor; omitempty]
502}
503
504struct ListPromptsResult {
505 prompts []Prompt
506 next_cursor string @[json: nextCursor; omitempty]
507}
508
509const default_tool_input_schema = '{"type":"object","additionalProperties":false}'
510
511// Server handles MCP protocol requests for stdio and HTTP transports.
512@[heap]
513pub struct Server {
514mut:
515 server_info Implementation
516 protocol_version string
517 capabilities_override string
518 instructions string
519 http_path string
520 allowed_origins []string
521 enable_logging bool
522 http_server &http.Server = unsafe { nil }
523 tools map[string]RegisteredTool
524 tool_names []string
525 resources map[string]RegisteredResource
526 resource_uris []string
527 resource_templates map[string]ResourceTemplate
528 resource_template_ids []string
529 prompts map[string]RegisteredPrompt
530 prompt_names []string
531 completions map[string]RegisteredCompletion
532 state shared ServerState
533}
534
535// new_server constructs a new MCP server.
536pub fn new_server(config ServerConfig) Server {
537 return Server{
538 server_info: normalize_server_info(config)
539 protocol_version: normalize_protocol_version(config.protocol_version)
540 capabilities_override: config.capabilities.trim_space()
541 instructions: config.instructions
542 http_path: normalize_http_path(config.http_path)
543 allowed_origins: config.allowed_origins.clone()
544 enable_logging: config.enable_logging
545 tools: map[string]RegisteredTool{}
546 resources: map[string]RegisteredResource{}
547 resource_templates: map[string]ResourceTemplate{}
548 prompts: map[string]RegisteredPrompt{}
549 completions: map[string]RegisteredCompletion{}
550 state: ServerState{}
551 }
552}
553
554// add_tool registers a tool and its handler.
555pub fn (mut s Server) add_tool(tool Tool, handler ToolHandler) ! {
556 validate_tool_name(tool.name)!
557 if tool.name in s.tools {
558 return error('mcp.Server.add_tool: duplicate tool `${tool.name}`')
559 }
560 normalized := Tool{
561 name: tool.name
562 title: tool.title
563 description: tool.description
564 input_schema: normalize_tool_input_schema(tool.input_schema)
565 output_schema: tool.output_schema.trim_space()
566 annotations: tool.annotations
567 icons: tool.icons
568 execution: tool.execution
569 }
570 s.tools[tool.name] = RegisteredTool{
571 tool: normalized
572 handler: handler
573 }
574 s.tool_names << tool.name
575 s.broadcast_notification('notifications/tools/list_changed', empty_object.str())
576}
577
578// add_resource registers a concrete resource and its read handler.
579pub fn (mut s Server) add_resource(resource Resource, handler ResourceHandler) ! {
580 if resource.uri.trim_space() == '' {
581 return error('mcp.Server.add_resource: empty uri')
582 }
583 if resource.uri in s.resources {
584 return error('mcp.Server.add_resource: duplicate resource `${resource.uri}`')
585 }
586 s.resources[resource.uri] = RegisteredResource{
587 resource: resource
588 handler: handler
589 }
590 s.resource_uris << resource.uri
591 s.broadcast_notification('notifications/resources/list_changed', empty_object.str())
592}
593
594// add_resource_template registers a resource template exposed by `resources/templates/list`.
595pub fn (mut s Server) add_resource_template(template ResourceTemplate) ! {
596 if template.uri_template.trim_space() == '' {
597 return error('mcp.Server.add_resource_template: empty uri template')
598 }
599 if template.uri_template in s.resource_templates {
600 return error('mcp.Server.add_resource_template: duplicate resource template `${template.uri_template}`')
601 }
602 s.resource_templates[template.uri_template] = template
603 s.resource_template_ids << template.uri_template
604 s.broadcast_notification('notifications/resources/list_changed', empty_object.str())
605}
606
607// add_prompt registers a prompt and its handler.
608pub fn (mut s Server) add_prompt(prompt Prompt, handler PromptHandler) ! {
609 if prompt.name.trim_space() == '' {
610 return error('mcp.Server.add_prompt: empty prompt name')
611 }
612 if prompt.name in s.prompts {
613 return error('mcp.Server.add_prompt: duplicate prompt `${prompt.name}`')
614 }
615 s.prompts[prompt.name] = RegisteredPrompt{
616 prompt: prompt
617 handler: handler
618 }
619 s.prompt_names << prompt.name
620 s.broadcast_notification('notifications/prompts/list_changed', empty_object.str())
621}
622
623// add_completion registers a completion handler for one argument of a prompt
624// or resource template. `argument` is the argument name being completed.
625pub fn (mut s Server) add_completion(ref CompletionRef, argument string, handler CompletionHandler) ! {
626 if argument.trim_space() == '' {
627 return error('mcp.Server.add_completion: empty argument name')
628 }
629 key := completion_key(ref, argument) or {
630 return error('mcp.Server.add_completion: ${err.msg()}')
631 }
632 if key in s.completions {
633 return error('mcp.Server.add_completion: duplicate completion `${key}`')
634 }
635 s.completions[key] = RegisteredCompletion{
636 ref: ref
637 argument: argument
638 handler: handler
639 }
640}
641
642fn completion_key(ref CompletionRef, argument string) !string {
643 match ref.ref_type {
644 'ref/prompt' {
645 if ref.name.trim_space() == '' {
646 return error('ref/prompt requires a `name`')
647 }
648 return 'prompt|${ref.name}|${argument}'
649 }
650 'ref/resource' {
651 if ref.uri.trim_space() == '' {
652 return error('ref/resource requires a `uri`')
653 }
654 return 'resource|${ref.uri}|${argument}'
655 }
656 else {
657 return error('unsupported ref type `${ref.ref_type}`')
658 }
659 }
660}
661
662// notify_tools_list_changed broadcasts the tool catalog change notification.
663pub fn (mut s Server) notify_tools_list_changed() {
664 s.broadcast_notification('notifications/tools/list_changed', empty_object.str())
665}
666
667// notify_resources_list_changed broadcasts the resource catalog change notification.
668pub fn (mut s Server) notify_resources_list_changed() {
669 s.broadcast_notification('notifications/resources/list_changed', empty_object.str())
670}
671
672// notify_prompts_list_changed broadcasts the prompt catalog change notification.
673pub fn (mut s Server) notify_prompts_list_changed() {
674 s.broadcast_notification('notifications/prompts/list_changed', empty_object.str())
675}
676
677// notify_log emits a `notifications/message` payload at `level` filtered per
678// session by the most recent `logging/setLevel` call. `data_json` MUST be a
679// valid JSON value (object preferred per spec). `logger` is optional.
680pub fn (mut s Server) notify_log(level LogLevel, logger string, data_json string) {
681 if !s.enable_logging {
682 return
683 }
684 mut fields := ['"level":${json.encode(level.str())}']
685 if logger != '' {
686 fields << '"logger":${json.encode(logger)}'
687 }
688 payload := if data_json.trim_space() == '' { 'null' } else { data_json.trim_space() }
689 fields << '"data":${payload}'
690 params := '{${fields.join(',')}}'
691 message := build_notification_message('notifications/message', params)
692 lock s.state {
693 for id in s.state.sessions.keys() {
694 mut session := s.state.sessions[id]
695 if !session.initialized {
696 continue
697 }
698 if session.log_level_set && int(level) < int(session.log_level) {
699 continue
700 }
701 session.notification_queue << message
702 s.state.sessions[id] = session
703 }
704 }
705}
706
707// list_roots issues `roots/list` to the session and waits for the response.
708pub fn (mut s Server) list_roots(session_id string, timeout time.Duration) !ListRootsResult {
709 response := s.send_server_request(session_id, 'roots/list', empty_object.str(), timeout)!
710 return response.decode_result[ListRootsResult]()
711}
712
713// sample issues `sampling/createMessage` and waits for the LLM result.
714pub fn (mut s Server) sample(session_id string, params CreateMessageParams, timeout time.Duration) !CreateMessageResult {
715 encoded := json.encode(params)
716 response := s.send_server_request(session_id, 'sampling/createMessage', encoded, timeout)!
717 return response.decode_result[CreateMessageResult]()
718}
719
720// elicit issues `elicitation/create` and waits for the user-supplied content.
721pub fn (mut s Server) elicit(session_id string, params ElicitParams, timeout time.Duration) !ElicitResult {
722 encoded := encode_elicit_params(params)
723 response := s.send_server_request(session_id, 'elicitation/create', encoded, timeout)!
724 return response.decode_result[ElicitResult]()
725}
726
727fn encode_elicit_params(params ElicitParams) string {
728 mut fields := []string{}
729 if params.mode != '' {
730 fields << '"mode":${json.encode(params.mode)}'
731 }
732 fields << '"message":${json.encode(params.message)}'
733 if params.url != '' {
734 fields << '"url":${json.encode(params.url)}'
735 }
736 if params.elicitation_id != '' {
737 fields << '"elicitationId":${json.encode(params.elicitation_id)}'
738 }
739 if params.requested_schema.properties.trim_space() != ''
740 || params.requested_schema.required.len > 0 {
741 fields << '"requestedSchema":${encode_elicit_schema(params.requested_schema)}'
742 }
743 return '{${fields.join(',')}}'
744}
745
746fn encode_elicit_schema(schema ElicitSchema) string {
747 type_value := if schema.type_.trim_space() == '' { 'object' } else { schema.type_ }
748 mut fields := ['"type":${json.encode(type_value)}']
749 if schema.properties.trim_space() != '' {
750 fields << '"properties":${schema.properties.trim_space()}'
751 }
752 if schema.required.len > 0 {
753 fields << '"required":[${schema.required.map(json.encode(it)).join(',')}]'
754 }
755 return '{${fields.join(',')}}'
756}
757
758fn (mut s Server) send_server_request(session_id string, method string, params_json string, timeout time.Duration) !Response {
759 request_id_quoted := s.allocate_server_request_id(session_id) or {
760 return error('mcp.Server.send_server_request: unknown session `${session_id}`')
761 }
762 encoded := Request{
763 id: request_id_quoted
764 method: method
765 params: params_json
766 }.encode()
767 lock s.state {
768 if session_id in s.state.sessions {
769 mut session := s.state.sessions[session_id]
770 session.notification_queue << encoded
771 s.state.sessions[session_id] = session
772 }
773 }
774 return s.wait_for_response(session_id, request_id_quoted, timeout)
775}
776
777fn (mut s Server) allocate_server_request_id(session_id string) ?string {
778 mut request_id := ''
779 lock s.state {
780 if session_id in s.state.sessions {
781 mut session := s.state.sessions[session_id]
782 request_id = '"server-${session.next_request_seq}"'
783 session.next_request_seq++
784 s.state.sessions[session_id] = session
785 s.state.response_signals[pending_signal_key(session_id, request_id)] =
786 sync.new_semaphore()
787 }
788 }
789 if request_id == '' {
790 return none
791 }
792 return request_id
793}
794
795fn (mut s Server) deliver_response(session_id string, response Response) {
796 key := pending_signal_key(session_id, response.id)
797 mut signal := unsafe { &sync.Semaphore(nil) }
798 lock s.state {
799 // Only stash the reply if a waiter is still listening on this id.
800 // `wait_for_response` removes its semaphore entry on timeout, so a
801 // missing signal means the request has been abandoned and any late
802 // reply must be dropped instead of accumulating in `pending_responses`.
803 signal = s.state.response_signals[key] or { unsafe { nil } }
804 if !isnil(signal) && session_id in s.state.sessions {
805 mut session := s.state.sessions[session_id]
806 session.pending_responses[response.id] = response
807 s.state.sessions[session_id] = session
808 }
809 }
810 if !isnil(signal) {
811 signal.post()
812 }
813}
814
815fn (mut s Server) wait_for_response(session_id string, request_id string, timeout time.Duration) !Response {
816 key := pending_signal_key(session_id, request_id)
817 mut signal := unsafe { &sync.Semaphore(nil) }
818 rlock s.state {
819 signal = s.state.response_signals[key] or { unsafe { nil } }
820 }
821 if isnil(signal) {
822 return error('mcp.Server.wait_for_response: no pending request `${request_id}`')
823 }
824 signaled := signal.timed_wait(timeout)
825 mut response := Response{}
826 mut found := false
827 lock s.state {
828 if session_id in s.state.sessions {
829 mut session := s.state.sessions[session_id]
830 if request_id in session.pending_responses {
831 response = session.pending_responses[request_id]
832 session.pending_responses.delete(request_id)
833 found = true
834 }
835 s.state.sessions[session_id] = session
836 }
837 s.state.response_signals.delete(key)
838 }
839 if found {
840 return response
841 }
842 if !signaled {
843 return error('mcp.Server.wait_for_response: timeout waiting for ${request_id}')
844 }
845 return error('mcp.Server.wait_for_response: session `${session_id}` closed while waiting for ${request_id}')
846}
847
848// notify_elicitation_complete emits a `notifications/elicitation/complete`
849// notification for the session that initiated a URL-mode elicitation. Pass
850// the same `elicitation_id` that was supplied to `elicit()` so the client
851// can correlate the out-of-band interaction with its pending request.
852pub fn (mut s Server) notify_elicitation_complete(session_id string, elicitation_id string) {
853 params := '{"elicitationId":${json.encode(elicitation_id)}}'
854 message := build_notification_message('notifications/elicitation/complete', params)
855 lock s.state {
856 if session_id in s.state.sessions {
857 mut session := s.state.sessions[session_id]
858 session.notification_queue << message
859 s.state.sessions[session_id] = session
860 }
861 }
862}
863
864// notify_resource_updated emits notifications/resources/updated to every
865// session that has subscribed to `uri`.
866pub fn (mut s Server) notify_resource_updated(uri string) {
867 params := '{"uri":${json.encode(uri)}}'
868 message := build_notification_message('notifications/resources/updated', params)
869 lock s.state {
870 for id in s.state.sessions.keys() {
871 mut session := s.state.sessions[id]
872 if !session.initialized {
873 continue
874 }
875 if uri !in session.subscribed_uris {
876 continue
877 }
878 session.notification_queue << message
879 s.state.sessions[id] = session
880 }
881 }
882}
883
884fn (mut s Server) subscribe(session_id string, uri string) {
885 lock s.state {
886 if session_id in s.state.sessions {
887 mut session := s.state.sessions[session_id]
888 if uri !in session.subscribed_uris {
889 session.subscribed_uris << uri
890 s.state.sessions[session_id] = session
891 }
892 }
893 }
894}
895
896fn (mut s Server) unsubscribe(session_id string, uri string) {
897 lock s.state {
898 if session_id in s.state.sessions {
899 mut session := s.state.sessions[session_id]
900 session.subscribed_uris = session.subscribed_uris.filter(it != uri)
901 s.state.sessions[session_id] = session
902 }
903 }
904}
905
906fn build_notification_message(method string, params_json string) string {
907 return Notification{
908 method: method
909 params: params_json
910 }.encode()
911}
912
913fn (mut s Server) broadcast_notification(method string, params_json string) {
914 message := build_notification_message(method, params_json)
915 lock s.state {
916 for id in s.state.sessions.keys() {
917 mut session := s.state.sessions[id]
918 if !session.initialized {
919 continue
920 }
921 session.notification_queue << message
922 s.state.sessions[id] = session
923 }
924 }
925}
926
927fn (mut s Server) drain_session_notifications(session_id string) []string {
928 mut messages := []string{}
929 lock s.state {
930 if session_id in s.state.sessions {
931 mut session := s.state.sessions[session_id]
932 messages = session.notification_queue.clone()
933 session.notification_queue = []string{}
934 s.state.sessions[session_id] = session
935 }
936 }
937 return messages
938}
939
940// drain_to_event_log moves queued notifications into the session's bounded
941// event log and returns the assigned (id, body) pairs ready for SSE framing.
942fn (mut s Server) drain_to_event_log(session_id string) []LoggedEvent {
943 mut events := []LoggedEvent{}
944 lock s.state {
945 if session_id in s.state.sessions {
946 mut session := s.state.sessions[session_id]
947 for body in session.notification_queue {
948 event := LoggedEvent{
949 id: session.next_event_id
950 body: body
951 }
952 session.next_event_id++
953 session.event_log << event
954 events << event
955 }
956 session.notification_queue = []string{}
957 if session.event_log.len > event_log_capacity {
958 session.event_log =
959 session.event_log[session.event_log.len - event_log_capacity..].clone()
960 }
961 s.state.sessions[session_id] = session
962 }
963 }
964 return events
965}
966
967fn (s &Server) replay_events_after(session_id string, last_event_id int) []LoggedEvent {
968 mut events := []LoggedEvent{}
969 rlock s.state {
970 if session_id in s.state.sessions {
971 session := s.state.sessions[session_id]
972 for event in session.event_log {
973 if event.id > last_event_id {
974 events << event
975 }
976 }
977 }
978 }
979 return events
980}
981
982// serve_stdio starts serving MCP messages over stdio using newline framing.
983// MCP 2025-11-25 mandates that stdio messages are delimited by newlines and
984// MUST NOT contain embedded newlines. We bypass libc stdio buffering on the
985// way in (raw `read()` on fd 0) and flush stdout after every frame on the way
986// out, so peers behind a pipe see responses immediately and the server
987// reacts to each line as soon as it arrives.
988pub fn (mut s Server) serve_stdio() ! {
989 mut stdout := os.stdout()
990 mut source := StdinReader{}
991 mut sink := StdioWriter{
992 file: &stdout
993 }
994 s.serve_stdio_transport(mut source, mut sink, stdio_session_id, .stdio)!
995}
996
997struct StdinReader {}
998
999fn (mut r StdinReader) read(mut buf []u8) !int {
1000 if buf.len == 0 {
1001 return io.Eof{}
1002 }
1003 n := unsafe { C.read(0, buf.data, buf.len) }
1004 if n == 0 {
1005 return io.Eof{}
1006 }
1007 if n < 0 {
1008 return error('mcp.stdio: read(0) failed')
1009 }
1010 return int(n)
1011}
1012
1013struct StdioWriter {
1014mut:
1015 file &os.File = unsafe { nil }
1016}
1017
1018fn (mut w StdioWriter) write(buf []u8) !int {
1019 n := w.file.write(buf)!
1020 w.file.flush()
1021 return n
1022}
1023
1024// serve_http starts serving MCP over a single HTTP endpoint.
1025pub fn (mut s Server) serve_http(addr string) ! {
1026 if addr.trim_space() == '' {
1027 return error('mcp.Server.serve_http: empty address')
1028 }
1029 mut handler := HttpHandler{
1030 server: s
1031 }
1032 mut http_server := &http.Server{
1033 addr: addr
1034 handler: handler
1035 accept_timeout: 100 * time.millisecond
1036 show_startup_message: false
1037 }
1038 s.http_server = http_server
1039 http_server.listen_and_serve()
1040}
1041
1042// close stops the HTTP server if it is running.
1043pub fn (mut s Server) close() {
1044 if !isnil(s.http_server) {
1045 s.http_server.close()
1046 }
1047}
1048
1049// wait_till_running waits until the HTTP server transitions to the running state.
1050pub fn (mut s Server) wait_till_running(params http.WaitTillRunningParams) !int {
1051 mut retries := 0
1052 for isnil(s.http_server) && retries < params.max_retries {
1053 time.sleep(params.retry_period_ms * time.millisecond)
1054 retries++
1055 }
1056 if isnil(s.http_server) {
1057 return error('mcp.Server.wait_till_running: HTTP server is not running')
1058 }
1059 remaining_retries := if params.max_retries > retries { params.max_retries - retries } else { 0 }
1060 if remaining_retries == 0 {
1061 return error('mcp.Server.wait_till_running: HTTP server is not running')
1062 }
1063 retry_count := s.http_server.wait_till_running(
1064 max_retries: remaining_retries
1065 retry_period_ms: params.retry_period_ms
1066 )!
1067 return retries + retry_count
1068}
1069
1070// text_content creates a raw MCP text content item.
1071pub fn text_content(text string) string {
1072 return text_content_with_annotations(text, Annotations{})
1073}
1074
1075// text_content_with_annotations behaves like `text_content` but attaches the
1076// provided `annotations` (audience, priority, lastModified) to the block.
1077pub fn text_content_with_annotations(text string, annotations Annotations) string {
1078 mut fields := ['"type":"text"', '"text":${json.encode(text)}']
1079 if encoded := encode_annotations(annotations) {
1080 fields << '"annotations":${encoded}'
1081 }
1082 return '{${fields.join(',')}}'
1083}
1084
1085// image_content creates a raw MCP image content block. `data` must be the
1086// base64-encoded payload and `mime_type` the IANA media type (e.g. `image/png`).
1087pub fn image_content(data string, mime_type string) string {
1088 return image_content_with_annotations(data, mime_type, Annotations{})
1089}
1090
1091// image_content_with_annotations behaves like `image_content` but attaches
1092// the provided `annotations` to the block.
1093pub fn image_content_with_annotations(data string, mime_type string, annotations Annotations) string {
1094 mut fields := ['"type":"image"', '"data":${json.encode(data)}',
1095 '"mimeType":${json.encode(mime_type)}']
1096 if encoded := encode_annotations(annotations) {
1097 fields << '"annotations":${encoded}'
1098 }
1099 return '{${fields.join(',')}}'
1100}
1101
1102// audio_content creates a raw MCP audio content block. `data` must be the
1103// base64-encoded payload and `mime_type` the IANA media type (e.g. `audio/wav`).
1104pub fn audio_content(data string, mime_type string) string {
1105 return audio_content_with_annotations(data, mime_type, Annotations{})
1106}
1107
1108// audio_content_with_annotations behaves like `audio_content` but attaches
1109// the provided `annotations` to the block.
1110pub fn audio_content_with_annotations(data string, mime_type string, annotations Annotations) string {
1111 mut fields := ['"type":"audio"', '"data":${json.encode(data)}',
1112 '"mimeType":${json.encode(mime_type)}']
1113 if encoded := encode_annotations(annotations) {
1114 fields << '"annotations":${encoded}'
1115 }
1116 return '{${fields.join(',')}}'
1117}
1118
1119// resource_link_content creates a raw MCP `resource_link` content block from
1120// a `Resource` description. The block carries the resource's metadata —
1121// including any `annotations` set on the resource — so the host can render
1122// or read it later.
1123pub fn resource_link_content(resource Resource) string {
1124 encoded := json.encode(resource)
1125 if encoded.len <= 2 {
1126 return '{"type":"resource_link"}'
1127 }
1128 return '{"type":"resource_link",${encoded[1..]}'
1129}
1130
1131// embedded_text_resource creates an embedded MCP text resource content block.
1132pub fn embedded_text_resource(uri string, mime_type string, text string) string {
1133 return embedded_text_resource_with_annotations(uri, mime_type, text, Annotations{})
1134}
1135
1136// embedded_text_resource_with_annotations behaves like `embedded_text_resource`
1137// but attaches the provided `annotations` to the outer `EmbeddedResource`.
1138pub fn embedded_text_resource_with_annotations(uri string, mime_type string, text string, annotations Annotations) string {
1139 inner := encode_resource_text_payload(uri, mime_type, text)
1140 return wrap_embedded_resource(inner, annotations)
1141}
1142
1143// embedded_blob_resource creates an embedded MCP binary resource content
1144// block. `blob` must be the base64-encoded payload.
1145pub fn embedded_blob_resource(uri string, mime_type string, blob string) string {
1146 return embedded_blob_resource_with_annotations(uri, mime_type, blob, Annotations{})
1147}
1148
1149// embedded_blob_resource_with_annotations behaves like `embedded_blob_resource`
1150// but attaches the provided `annotations` to the outer `EmbeddedResource`.
1151pub fn embedded_blob_resource_with_annotations(uri string, mime_type string, blob string, annotations Annotations) string {
1152 inner := encode_resource_blob_payload(uri, mime_type, blob)
1153 return wrap_embedded_resource(inner, annotations)
1154}
1155
1156fn encode_resource_text_payload(uri string, mime_type string, text string) string {
1157 encoded_uri := json.encode(uri)
1158 encoded_text := json.encode(text)
1159 if mime_type == '' {
1160 return '{"uri":${encoded_uri},"text":${encoded_text}}'
1161 }
1162 encoded_mime := json.encode(mime_type)
1163 return '{"uri":${encoded_uri},"mimeType":${encoded_mime},"text":${encoded_text}}'
1164}
1165
1166fn encode_resource_blob_payload(uri string, mime_type string, blob string) string {
1167 encoded_uri := json.encode(uri)
1168 encoded_blob := json.encode(blob)
1169 if mime_type == '' {
1170 return '{"uri":${encoded_uri},"blob":${encoded_blob}}'
1171 }
1172 encoded_mime := json.encode(mime_type)
1173 return '{"uri":${encoded_uri},"mimeType":${encoded_mime},"blob":${encoded_blob}}'
1174}
1175
1176fn wrap_embedded_resource(inner string, annotations Annotations) string {
1177 mut fields := ['"type":"resource"', '"resource":${inner}']
1178 if encoded := encode_annotations(annotations) {
1179 fields << '"annotations":${encoded}'
1180 }
1181 return '{${fields.join(',')}}'
1182}
1183
1184// encode_annotations renders an `Annotations` value as JSON, omitting the
1185// whole object when no field is populated (returns `none`).
1186fn encode_annotations(annotations Annotations) ?string {
1187 mut fields := []string{}
1188 if annotations.audience.len > 0 {
1189 fields << '"audience":[${annotations.audience.map(json.encode(it)).join(',')}]'
1190 }
1191 if priority := annotations.priority {
1192 fields << '"priority":${format_number(priority)}'
1193 }
1194 if annotations.last_modified != '' {
1195 fields << '"lastModified":${json.encode(annotations.last_modified)}'
1196 }
1197 if fields.len == 0 {
1198 return none
1199 }
1200 return '{${fields.join(',')}}'
1201}
1202
1203// prompt_text_message creates a prompt message with text content.
1204pub fn prompt_text_message(role string, text string) PromptMessage {
1205 return PromptMessage{
1206 role: role
1207 content: text_content(text)
1208 }
1209}
1210
1211// tool_text_result wraps plain text in an MCP tool result.
1212pub fn tool_text_result(text string) ToolResult {
1213 return ToolResult{
1214 content: '[${text_content(text)}]'
1215 }
1216}
1217
1218fn tool_error_result(text string) ToolResult {
1219 return ToolResult{
1220 content: '[${text_content(text)}]'
1221 is_error: true
1222 }
1223}
1224
1225fn response_with_json(request_id string, result_json string) Response {
1226 return Response{
1227 id: request_id
1228 result: result_json
1229 }
1230}
1231
1232fn encode_initialize_result(result InitializeResult) string {
1233 mut fields := [
1234 '"protocolVersion":${json.encode(result.protocol_version)}',
1235 '"capabilities":${normalize_capabilities(result.capabilities)}',
1236 '"serverInfo":${json.encode(result.server_info)}',
1237 ]
1238 if result.instructions != '' {
1239 fields << '"instructions":${json.encode(result.instructions)}'
1240 }
1241 return '{${fields.join(',')}}'
1242}
1243
1244fn encode_tools_list_result(result ListToolsResult) string {
1245 mut fields := ['"tools":[${result.tools.map(encode_tool).join(',')}]']
1246 if result.next_cursor != '' {
1247 fields << '"nextCursor":${json.encode(result.next_cursor)}'
1248 }
1249 return '{${fields.join(',')}}'
1250}
1251
1252fn encode_tool(tool Tool) string {
1253 mut fields := ['"name":${json.encode(tool.name)}',
1254 '"inputSchema":${normalize_tool_input_schema(tool.input_schema)}']
1255 if tool.title != '' {
1256 fields << '"title":${json.encode(tool.title)}'
1257 }
1258 if tool.description != '' {
1259 fields << '"description":${json.encode(tool.description)}'
1260 }
1261 if tool.output_schema.trim_space() != '' {
1262 fields << '"outputSchema":${tool.output_schema.trim_space()}'
1263 }
1264 if encoded_annotations := encode_tool_annotations(tool.annotations) {
1265 fields << '"annotations":${encoded_annotations}'
1266 }
1267 if tool.icons.len > 0 {
1268 fields << '"icons":${json.encode(tool.icons)}'
1269 }
1270 if tool.execution.task_support != '' {
1271 fields << '"execution":{"taskSupport":${json.encode(tool.execution.task_support)}}'
1272 }
1273 return '{${fields.join(',')}}'
1274}
1275
1276fn encode_tool_annotations(annotations ToolAnnotations) ?string {
1277 mut fields := []string{}
1278 if annotations.title != '' {
1279 fields << '"title":${json.encode(annotations.title)}'
1280 }
1281 if hint := annotations.read_only_hint {
1282 fields << '"readOnlyHint":${hint.str()}'
1283 }
1284 if hint := annotations.destructive_hint {
1285 fields << '"destructiveHint":${hint.str()}'
1286 }
1287 if hint := annotations.idempotent_hint {
1288 fields << '"idempotentHint":${hint.str()}'
1289 }
1290 if hint := annotations.open_world_hint {
1291 fields << '"openWorldHint":${hint.str()}'
1292 }
1293 if fields.len == 0 {
1294 return none
1295 }
1296 return '{${fields.join(',')}}'
1297}
1298
1299fn encode_tool_result(result ToolResult) string {
1300 // `content` is REQUIRED by the 2025-11-25 schema (CallToolResult.content),
1301 // so always emit at least an empty array; clients reading the result MUST
1302 // be able to find `content` whether or not the tool produced output.
1303 content_payload := if result.content.trim_space() == '' {
1304 '[]'
1305 } else {
1306 result.content.trim_space()
1307 }
1308 mut fields := ['"content":${content_payload}', '"isError":${result.is_error.str()}']
1309 if result.structured_content.trim_space() != '' {
1310 fields << '"structuredContent":${result.structured_content.trim_space()}'
1311 }
1312 return '{${fields.join(',')}}'
1313}
1314
1315fn encode_prompt_result(result GetPromptResult) string {
1316 mut fields := [
1317 '"messages":[${result.messages.map(encode_prompt_message).join(',')}]',
1318 ]
1319 if result.description != '' {
1320 fields << '"description":${json.encode(result.description)}'
1321 }
1322 return '{${fields.join(',')}}'
1323}
1324
1325fn encode_prompt_message(message PromptMessage) string {
1326 return '{"role":${json.encode(message.role)},"content":${message.content}}'
1327}
1328
1329fn (mut s Server) serve_stdio_transport(mut reader io.Reader, mut writer io.Writer, session_id string, transport SessionTransport) ! {
1330 mut buffer := ''
1331 for {
1332 frame := try_extract_stdio_message(buffer) or {
1333 if err.msg() != NoFrameError{}.msg() {
1334 error_response := Response{
1335 error: normalize_response_error(err)
1336 }.encode()
1337 writer.write(encode_stdio_message(error_response).bytes())!
1338 return err
1339 }
1340 FrameExtraction{}
1341 }
1342 if frame.message.len != 0 {
1343 buffer = frame.remaining
1344 dispatch_result := s.dispatch_message(frame.message, session_id, transport) or {
1345 error_response := Response{
1346 error: normalize_response_error(err)
1347 }.encode()
1348 writer.write(encode_stdio_message(error_response).bytes())!
1349 continue
1350 }
1351 if dispatch_result.has_response {
1352 writer.write(encode_stdio_message(dispatch_result.response).bytes())!
1353 }
1354 for notification in s.drain_session_notifications(session_id) {
1355 writer.write(encode_stdio_message(notification).bytes())!
1356 }
1357 continue
1358 }
1359 mut chunk := []u8{len: 4096}
1360 bytes_read := reader.read(mut chunk) or {
1361 if err is os.Eof {
1362 return
1363 }
1364 if err is io.Eof {
1365 return
1366 }
1367 return err
1368 }
1369 if bytes_read == 0 {
1370 return
1371 }
1372 buffer += chunk[..bytes_read].bytestr()
1373 }
1374}
1375
1376fn (mut s Server) dispatch_message(raw string, session_id string, transport SessionTransport) !DispatchResult {
1377 trimmed := raw.trim_space()
1378 if trimmed.len == 0 || trimmed[0] == `[` {
1379 return ProtocolError{
1380 response_error: invalid_request
1381 }
1382 }
1383 envelope := decode_envelope(trimmed) or {
1384 return ProtocolError{
1385 response_error: parse_error
1386 }
1387 }
1388 return s.dispatch_envelope(envelope, session_id, transport)
1389}
1390
1391fn (mut s Server) dispatch_envelope(envelope MessageEnvelope, session_id string, transport SessionTransport) !DispatchResult {
1392 if envelope.method.len == 0 {
1393 if !is_notification_id(envelope.id) {
1394 s.deliver_response(session_id, Response{
1395 id: envelope.id
1396 result: envelope.result
1397 error: envelope.error
1398 })
1399 }
1400 return DispatchResult{}
1401 }
1402 if is_notification_id(envelope.id) {
1403 s.handle_notification(Notification{
1404 method: envelope.method
1405 params: envelope.params
1406 }, session_id, transport)!
1407 return DispatchResult{}
1408 }
1409 req := Request{
1410 id: envelope.id
1411 method: envelope.method
1412 params: envelope.params
1413 }
1414 handled := s.handle_request(req, session_id, transport)
1415 return DispatchResult{
1416 has_response: true
1417 response: handled.response.encode()
1418 session_id: handled.session_id
1419 }
1420}
1421
1422fn (mut s Server) handle_request(req Request, session_id string, transport SessionTransport) HandledRequest {
1423 defer {
1424 s.clear_cancelled(session_id, req.id)
1425 }
1426 return s.handle_request_impl(req, session_id, transport) or {
1427 return HandledRequest{
1428 response: error_response_for(req.id, err)
1429 }
1430 }
1431}
1432
1433fn (mut s Server) handle_request_impl(req Request, session_id string, transport SessionTransport) !HandledRequest {
1434 if req.method == 'ping' {
1435 return HandledRequest{
1436 response: response_with_json(req.id, empty_object.str())
1437 }
1438 }
1439 if req.method == 'initialize' {
1440 return s.handle_initialize(req, session_id, transport)
1441 }
1442 session := s.session_for_request(session_id, transport) or {
1443 return ProtocolError{
1444 response_error: server_not_initialized
1445 request_id: req.id
1446 }
1447 }
1448 if !session.initialize_complete || !session.initialized {
1449 return ProtocolError{
1450 response_error: server_not_initialized
1451 request_id: req.id
1452 }
1453 }
1454 ctx := s.context_from_session(req, session)
1455 match req.method {
1456 'tools/list' {
1457 return s.handle_tools_list(req)
1458 }
1459 'tools/call' {
1460 return s.handle_tools_call(req, ctx)
1461 }
1462 'resources/list' {
1463 return s.handle_resources_list(req)
1464 }
1465 'resources/read' {
1466 return s.handle_resources_read(req, ctx)
1467 }
1468 'resources/templates/list' {
1469 return s.handle_resource_templates_list(req)
1470 }
1471 'resources/subscribe' {
1472 return s.handle_resources_subscribe(req, session_id)
1473 }
1474 'resources/unsubscribe' {
1475 return s.handle_resources_unsubscribe(req, session_id)
1476 }
1477 'logging/setLevel' {
1478 return s.handle_logging_set_level(req, session_id)
1479 }
1480 'completion/complete' {
1481 return s.handle_completion_complete(req, ctx)
1482 }
1483 'prompts/list' {
1484 return s.handle_prompts_list(req)
1485 }
1486 'prompts/get' {
1487 return s.handle_prompts_get(req, ctx)
1488 }
1489 else {
1490 return ProtocolError{
1491 response_error: method_not_found
1492 request_id: req.id
1493 }
1494 }
1495 }
1496}
1497
1498fn (mut s Server) handle_initialize(req Request, session_id string, transport SessionTransport) !HandledRequest {
1499 params := req.decode_params[InitializeParams]() or {
1500 return ProtocolError{
1501 response_error: invalid_params
1502 request_id: req.id
1503 }
1504 }
1505 mut session := s.ensure_session_for_initialize(session_id, transport)
1506 if session.initialize_complete {
1507 return ProtocolError{
1508 response_error: invalid_request
1509 request_id: req.id
1510 }
1511 }
1512 // Per MCP lifecycle: server replies with the protocol version it supports;
1513 // the client decides whether to proceed or disconnect on a mismatch.
1514 session.protocol_version = s.protocol_version
1515 session.client_info = normalize_client_info(params.client_info)
1516 session.client_capabilities = normalize_capabilities(params.capabilities)
1517 session.initialize_complete = true
1518 session.initialized = false
1519 s.store_session(session)
1520 result := InitializeResult{
1521 protocol_version: session.protocol_version
1522 capabilities: s.capabilities_json()
1523 server_info: s.server_info
1524 instructions: s.instructions
1525 }
1526 return HandledRequest{
1527 response: response_with_json(req.id, encode_initialize_result(result))
1528 session_id: if transport == .http { session.id } else { '' }
1529 }
1530}
1531
1532fn (mut s Server) handle_tools_list(req Request) !HandledRequest {
1533 params := decode_optional_params[CursorParams](req.params) or {
1534 return ProtocolError{
1535 response_error: invalid_params
1536 request_id: req.id
1537 }
1538 }
1539 start, end, next_cursor := paginate_bounds(s.tool_names.len, params.cursor)!
1540 mut tools := []Tool{cap: end - start}
1541 for name in s.tool_names[start..end] {
1542 tools << s.tools[name].tool
1543 }
1544 return HandledRequest{
1545 response: response_with_json(req.id, encode_tools_list_result(ListToolsResult{
1546 tools: tools
1547 next_cursor: next_cursor
1548 }))
1549 }
1550}
1551
1552fn (mut s Server) handle_tools_call(req Request, ctx Context) !HandledRequest {
1553 params := decode_optional_params[ToolCallParams](req.params) or {
1554 return ProtocolError{
1555 response_error: invalid_params
1556 request_id: req.id
1557 }
1558 }
1559 if params.name !in s.tools {
1560 return ProtocolError{
1561 response_error: invalid_params
1562 request_id: req.id
1563 }
1564 }
1565 entry := s.tools[params.name]
1566 result := entry.handler(ctx, json_object_or_empty(params.arguments)) or {
1567 if err is ProtocolError {
1568 return err
1569 }
1570 if err is ResponseError {
1571 return err
1572 }
1573 return HandledRequest{
1574 response: response_with_json(req.id, encode_tool_result(tool_error_result(err.msg())))
1575 }
1576 }
1577 return HandledRequest{
1578 response: response_with_json(req.id, encode_tool_result(result))
1579 }
1580}
1581
1582fn (mut s Server) handle_resources_list(req Request) !HandledRequest {
1583 params := decode_optional_params[CursorParams](req.params) or {
1584 return ProtocolError{
1585 response_error: invalid_params
1586 request_id: req.id
1587 }
1588 }
1589 start, end, next_cursor := paginate_bounds(s.resource_uris.len, params.cursor)!
1590 mut resources := []Resource{cap: end - start}
1591 for uri in s.resource_uris[start..end] {
1592 resources << s.resources[uri].resource
1593 }
1594 return HandledRequest{
1595 response: response_with_json(req.id, encode_value(ListResourcesResult{
1596 resources: resources
1597 next_cursor: next_cursor
1598 }))
1599 }
1600}
1601
1602fn (mut s Server) handle_resources_read(req Request, ctx Context) !HandledRequest {
1603 params := req.decode_params[ReadResourceParams]() or {
1604 return ProtocolError{
1605 response_error: invalid_params
1606 request_id: req.id
1607 }
1608 }
1609 if params.uri !in s.resources {
1610 return ProtocolError{
1611 response_error: ResponseError{
1612 code: resource_not_found.code
1613 message: 'Resource not found.'
1614 data: '{"uri":${json.encode(params.uri)}}'
1615 }
1616 request_id: req.id
1617 }
1618 }
1619 entry := s.resources[params.uri]
1620 result := entry.handler(ctx, params.uri) or { return err }
1621 return HandledRequest{
1622 response: response_with_json(req.id, encode_value(result))
1623 }
1624}
1625
1626struct SetLevelParams {
1627 level string
1628}
1629
1630struct CompletionRequestArgument {
1631 name string
1632 value string
1633}
1634
1635struct CompletionRequestContext {
1636 arguments string @[raw]
1637}
1638
1639struct CompletionRequestParams {
1640 ref CompletionRef
1641 argument CompletionRequestArgument
1642 context CompletionRequestContext
1643}
1644
1645fn (mut s Server) handle_completion_complete(req Request, ctx Context) !HandledRequest {
1646 if s.completions.len == 0 {
1647 return ProtocolError{
1648 response_error: method_not_found
1649 request_id: req.id
1650 }
1651 }
1652 params := req.decode_params[CompletionRequestParams]() or {
1653 return ProtocolError{
1654 response_error: invalid_params
1655 request_id: req.id
1656 }
1657 }
1658 key := completion_key(params.ref, params.argument.name) or {
1659 return ProtocolError{
1660 response_error: invalid_params
1661 request_id: req.id
1662 }
1663 }
1664 entry := s.completions[key] or {
1665 return HandledRequest{
1666 response: response_with_json(req.id, encode_completion_result(CompletionResult{}))
1667 }
1668 }
1669 arguments_json := json_object_or_empty(params.context.arguments)
1670 result := entry.handler(ctx, params.argument.value, arguments_json) or {
1671 if err is ProtocolError {
1672 return err
1673 }
1674 if err is ResponseError {
1675 return err
1676 }
1677 return ProtocolError{
1678 response_error: ResponseError{
1679 code: internal_error.code
1680 message: err.msg()
1681 }
1682 request_id: req.id
1683 }
1684 }
1685 return HandledRequest{
1686 response: response_with_json(req.id, encode_completion_result(result))
1687 }
1688}
1689
1690fn encode_completion_result(result CompletionResult) string {
1691 // MCP 2025-11-25 caps completion responses at 100 values. We clamp here
1692 // and surface the truncation through `hasMore: true` so clients keep
1693 // requesting refinements instead of silently losing options.
1694 clamped := if result.values.len > completion_values_limit {
1695 result.values[..completion_values_limit]
1696 } else {
1697 result.values
1698 }
1699 mut completion_fields := [
1700 '"values":[${clamped.map(json.encode(it)).join(',')}]',
1701 ]
1702 if total := result.total {
1703 completion_fields << '"total":${total}'
1704 }
1705 mut has_more_value := result.values.len > completion_values_limit
1706 if explicit := result.has_more {
1707 has_more_value = explicit || has_more_value
1708 }
1709 if result.has_more != none || has_more_value {
1710 completion_fields << '"hasMore":${has_more_value.str()}'
1711 }
1712 return '{"completion":{${completion_fields.join(',')}}}'
1713}
1714
1715fn (mut s Server) handle_logging_set_level(req Request, session_id string) !HandledRequest {
1716 if !s.enable_logging {
1717 return ProtocolError{
1718 response_error: method_not_found
1719 request_id: req.id
1720 }
1721 }
1722 params := req.decode_params[SetLevelParams]() or {
1723 return ProtocolError{
1724 response_error: invalid_params
1725 request_id: req.id
1726 }
1727 }
1728 level := parse_log_level(params.level) or {
1729 return ProtocolError{
1730 response_error: invalid_params
1731 request_id: req.id
1732 }
1733 }
1734 s.set_session_log_level(session_id, level)
1735 return HandledRequest{
1736 response: response_with_json(req.id, empty_object.str())
1737 }
1738}
1739
1740fn (mut s Server) set_session_log_level(session_id string, level LogLevel) {
1741 lock s.state {
1742 if session_id in s.state.sessions {
1743 mut session := s.state.sessions[session_id]
1744 session.log_level = level
1745 session.log_level_set = true
1746 s.state.sessions[session_id] = session
1747 }
1748 }
1749}
1750
1751fn (mut s Server) handle_resources_subscribe(req Request, session_id string) !HandledRequest {
1752 params := req.decode_params[SubscribeParams]() or {
1753 return ProtocolError{
1754 response_error: invalid_params
1755 request_id: req.id
1756 }
1757 }
1758 if params.uri.trim_space() == '' {
1759 return ProtocolError{
1760 response_error: invalid_params
1761 request_id: req.id
1762 }
1763 }
1764 s.subscribe(session_id, params.uri)
1765 return HandledRequest{
1766 response: response_with_json(req.id, empty_object.str())
1767 }
1768}
1769
1770fn (mut s Server) handle_resources_unsubscribe(req Request, session_id string) !HandledRequest {
1771 params := req.decode_params[SubscribeParams]() or {
1772 return ProtocolError{
1773 response_error: invalid_params
1774 request_id: req.id
1775 }
1776 }
1777 s.unsubscribe(session_id, params.uri)
1778 return HandledRequest{
1779 response: response_with_json(req.id, empty_object.str())
1780 }
1781}
1782
1783fn (mut s Server) handle_resource_templates_list(req Request) !HandledRequest {
1784 params := decode_optional_params[CursorParams](req.params) or {
1785 return ProtocolError{
1786 response_error: invalid_params
1787 request_id: req.id
1788 }
1789 }
1790 start, end, next_cursor := paginate_bounds(s.resource_template_ids.len, params.cursor)!
1791 mut templates := []ResourceTemplate{cap: end - start}
1792 for uri_template in s.resource_template_ids[start..end] {
1793 templates << s.resource_templates[uri_template]
1794 }
1795 return HandledRequest{
1796 response: response_with_json(req.id, encode_value(ListResourceTemplatesResult{
1797 resource_templates: templates
1798 next_cursor: next_cursor
1799 }))
1800 }
1801}
1802
1803fn (mut s Server) handle_prompts_list(req Request) !HandledRequest {
1804 params := decode_optional_params[CursorParams](req.params) or {
1805 return ProtocolError{
1806 response_error: invalid_params
1807 request_id: req.id
1808 }
1809 }
1810 start, end, next_cursor := paginate_bounds(s.prompt_names.len, params.cursor)!
1811 mut prompts := []Prompt{cap: end - start}
1812 for name in s.prompt_names[start..end] {
1813 prompts << s.prompts[name].prompt
1814 }
1815 return HandledRequest{
1816 response: response_with_json(req.id, encode_value(ListPromptsResult{
1817 prompts: prompts
1818 next_cursor: next_cursor
1819 }))
1820 }
1821}
1822
1823fn (mut s Server) handle_prompts_get(req Request, ctx Context) !HandledRequest {
1824 params := decode_optional_params[GetPromptParams](req.params) or {
1825 return ProtocolError{
1826 response_error: invalid_params
1827 request_id: req.id
1828 }
1829 }
1830 if params.name !in s.prompts {
1831 return ProtocolError{
1832 response_error: invalid_params
1833 request_id: req.id
1834 }
1835 }
1836 entry := s.prompts[params.name]
1837 result := entry.handler(ctx, json_object_or_empty(params.arguments)) or { return err }
1838 return HandledRequest{
1839 response: response_with_json(req.id, encode_prompt_result(result))
1840 }
1841}
1842
1843fn (mut s Server) handle_notification(notification Notification, session_id string, transport SessionTransport) ! {
1844 match notification.method {
1845 'notifications/initialized' {
1846 mut session := s.session_for_request(session_id, transport) or {
1847 return ProtocolError{
1848 response_error: server_not_initialized
1849 }
1850 }
1851 if !session.initialize_complete {
1852 return ProtocolError{
1853 response_error: server_not_initialized
1854 }
1855 }
1856 session.initialized = true
1857 s.store_session(session)
1858 }
1859 'notifications/cancelled' {
1860 params := notification.decode_params[CancelledParams]() or { return }
1861 s.mark_cancelled(session_id, params.request_id)
1862 }
1863 else {}
1864 }
1865}
1866
1867struct CancelledParams {
1868 request_id string @[json: requestId; raw]
1869 reason string @[omitempty]
1870}
1871
1872fn (mut s Server) mark_cancelled(session_id string, request_id string) {
1873 id := request_id.trim_space()
1874 if id.len == 0 {
1875 return
1876 }
1877 lock s.state {
1878 if session_id in s.state.sessions {
1879 mut session := s.state.sessions[session_id]
1880 if id !in session.cancelled_requests {
1881 session.cancelled_requests << id
1882 s.state.sessions[session_id] = session
1883 }
1884 }
1885 }
1886}
1887
1888fn (s &Server) is_request_cancelled(session_id string, request_id string) bool {
1889 mut cancelled := false
1890 rlock s.state {
1891 if session_id in s.state.sessions {
1892 cancelled = request_id in s.state.sessions[session_id].cancelled_requests
1893 }
1894 }
1895 return cancelled
1896}
1897
1898fn (mut s Server) clear_cancelled(session_id string, request_id string) {
1899 lock s.state {
1900 if session_id in s.state.sessions {
1901 mut session := s.state.sessions[session_id]
1902 session.cancelled_requests = session.cancelled_requests.filter(it != request_id)
1903 s.state.sessions[session_id] = session
1904 }
1905 }
1906}
1907
1908fn (s &Server) notify_progress_for(session_id string, progress_token string, progress f64, total f64, message string) {
1909 mut fields := [
1910 '"progressToken":${progress_token}',
1911 '"progress":${format_number(progress)}',
1912 ]
1913 if total != 0 {
1914 fields << '"total":${format_number(total)}'
1915 }
1916 if message != '' {
1917 fields << '"message":${json.encode(message)}'
1918 }
1919 params := '{${fields.join(',')}}'
1920 notification := build_notification_message('notifications/progress', params)
1921 lock s.state {
1922 if session_id !in s.state.sessions {
1923 return
1924 }
1925 mut session := s.state.sessions[session_id]
1926 // MCP 2025-11-25 mandates `progress` strictly increases per token.
1927 if last := session.progress_seen[progress_token] {
1928 if progress <= last {
1929 return
1930 }
1931 }
1932 session.progress_seen[progress_token] = progress
1933 session.notification_queue << notification
1934 s.state.sessions[session_id] = session
1935 }
1936}
1937
1938fn format_number(value f64) string {
1939 if value == f64(i64(value)) {
1940 return i64(value).str()
1941 }
1942 return value.str()
1943}
1944
1945fn (s &Server) capabilities_json() string {
1946 if s.capabilities_override != '' {
1947 return normalize_capabilities(s.capabilities_override)
1948 }
1949 mut parts := []string{}
1950 if s.tool_names.len != 0 {
1951 parts << '"tools":{"listChanged":true}'
1952 }
1953 if s.resource_uris.len != 0 || s.resource_template_ids.len != 0 {
1954 parts << '"resources":{"listChanged":true,"subscribe":true}'
1955 }
1956 if s.prompt_names.len != 0 {
1957 parts << '"prompts":{"listChanged":true}'
1958 }
1959 if s.enable_logging {
1960 parts << '"logging":{}'
1961 }
1962 if s.completions.len != 0 {
1963 parts << '"completions":{}'
1964 }
1965 return '{${parts.join(',')}}'
1966}
1967
1968fn (mut s Server) ensure_session_for_initialize(session_id string, transport SessionTransport) Session {
1969 if transport == .stdio {
1970 return s.ensure_session(stdio_session_id, .stdio)
1971 }
1972 if session_id != '' {
1973 return s.ensure_session(session_id, .http)
1974 }
1975 return s.create_http_session()
1976}
1977
1978fn (mut s Server) ensure_session(session_id string, transport SessionTransport) Session {
1979 if session := s.get_session(session_id) {
1980 return session
1981 }
1982 session := Session{
1983 id: session_id
1984 transport: transport
1985 protocol_version: s.protocol_version
1986 }
1987 s.store_session(session)
1988 return session
1989}
1990
1991fn (mut s Server) create_http_session() Session {
1992 for {
1993 session_id := rand.uuid_v7()
1994 if !s.session_exists(session_id) {
1995 session := Session{
1996 id: session_id
1997 transport: .http
1998 protocol_version: s.protocol_version
1999 }
2000 s.store_session(session)
2001 return session
2002 }
2003 }
2004 return Session{}
2005}
2006
2007fn (s &Server) session_for_request(session_id string, transport SessionTransport) ?Session {
2008 if transport == .stdio {
2009 return s.get_session(stdio_session_id)
2010 }
2011 if session_id == '' {
2012 return none
2013 }
2014 return s.get_session(session_id)
2015}
2016
2017fn (mut s Server) store_session(session Session) {
2018 lock s.state {
2019 s.state.sessions[session.id] = session
2020 }
2021}
2022
2023fn (s &Server) get_session(session_id string) ?Session {
2024 mut session := Session{}
2025 mut found := false
2026 rlock s.state {
2027 if session_id in s.state.sessions {
2028 session = s.state.sessions[session_id]
2029 found = true
2030 }
2031 }
2032 if !found {
2033 return none
2034 }
2035 return session
2036}
2037
2038fn (s &Server) session_exists(session_id string) bool {
2039 mut found := false
2040 rlock s.state {
2041 found = session_id in s.state.sessions
2042 }
2043 return found
2044}
2045
2046fn (mut s Server) delete_session(session_id string) bool {
2047 mut deleted := false
2048 mut orphaned_signals := []&sync.Semaphore{}
2049 prefix := pending_signal_key(session_id, '')
2050 lock s.state {
2051 if session_id in s.state.sessions {
2052 s.state.sessions.delete(session_id)
2053 deleted = true
2054 }
2055 for key, signal in s.state.response_signals {
2056 if key.starts_with(prefix) {
2057 orphaned_signals << signal
2058 }
2059 }
2060 }
2061 for mut signal in orphaned_signals {
2062 signal.post()
2063 }
2064 return deleted
2065}
2066
2067fn (s &Server) context_from_session(req Request, session Session) Context {
2068 return Context{
2069 session_id: session.id
2070 request_id: req.id
2071 method: req.method
2072 transport: session.transport
2073 protocol_version: session.protocol_version
2074 client_info: session.client_info
2075 client_capabilities: session.client_capabilities
2076 progress_token: extract_progress_token(req.params)
2077 server: unsafe { s }
2078 }
2079}
2080
2081struct MetaParams {
2082 meta MetaPayload @[json: '_meta']
2083}
2084
2085struct MetaPayload {
2086 progress_token string @[json: progressToken; raw]
2087}
2088
2089fn extract_progress_token(params string) string {
2090 trimmed := params.trim_space()
2091 if trimmed.len == 0 || trimmed == null.str() {
2092 return ''
2093 }
2094 wrapper := json.decode(MetaParams, trimmed) or { return '' }
2095 return wrapper.meta.progress_token.trim_space()
2096}
2097
2098fn decode_optional_params[T](raw string) !T {
2099 trimmed := raw.trim_space()
2100 if trimmed.len == 0 || trimmed == null.str() {
2101 return T{}
2102 }
2103 return decode_value[T](trimmed)
2104}
2105
2106fn json_object_or_empty(raw string) string {
2107 trimmed := raw.trim_space()
2108 if trimmed.len == 0 || trimmed == null.str() {
2109 return '{}'
2110 }
2111 return trimmed
2112}
2113
2114fn paginate_bounds(total int, cursor string) !(int, int, string) {
2115 mut start := 0
2116 if cursor.trim_space() != '' {
2117 start = cursor.int()
2118 if start < 0 || start > total || (start == 0 && cursor != '0') {
2119 return ProtocolError{
2120 response_error: invalid_params
2121 }
2122 }
2123 }
2124 mut end := start + default_list_page_size
2125 if end > total {
2126 end = total
2127 }
2128 next_cursor := if end < total { end.str() } else { '' }
2129 return start, end, next_cursor
2130}
2131
2132fn validate_tool_name(name string) ! {
2133 trimmed := name.trim_space()
2134 if trimmed.len == 0 || trimmed.len > 128 {
2135 return error('mcp.Server.add_tool: invalid tool name `${name}`')
2136 }
2137 for ch in trimmed {
2138 if ch.is_letter() || ch.is_digit() || ch in [`_`, `-`, `.`] {
2139 continue
2140 }
2141 return error('mcp.Server.add_tool: invalid tool name `${name}`')
2142 }
2143}
2144
2145fn normalize_tool_input_schema(input_schema string) string {
2146 trimmed := input_schema.trim_space()
2147 return if trimmed.len == 0 { default_tool_input_schema } else { trimmed }
2148}
2149
2150fn normalize_server_info(config ServerConfig) Implementation {
2151 return Implementation{
2152 name: if config.name.trim_space() == '' {
2153 default_server_name
2154 } else {
2155 config.name.trim_space()
2156 }
2157 version: if config.version.trim_space() == '' {
2158 default_server_version
2159 } else {
2160 config.version.trim_space()
2161 }
2162 title: config.title.trim_space()
2163 description: config.description.trim_space()
2164 website_url: config.website_url.trim_space()
2165 icons: config.icons.clone()
2166 }
2167}
2168
2169fn normalize_http_path(path string) string {
2170 trimmed := path.trim_space()
2171 if trimmed == '' {
2172 return default_http_path
2173 }
2174 return if trimmed.starts_with('/') { trimmed } else { '/' + trimmed }
2175}
2176
2177fn error_response_for(request_id string, err IError) Response {
2178 mut response_id := request_id
2179 response_error := normalize_response_error(err)
2180 if err is ProtocolError && err.request_id != '' {
2181 response_id = err.request_id
2182 }
2183 return Response{
2184 id: response_id
2185 error: response_error
2186 }
2187}
2188
2189fn normalize_response_error(err IError) ResponseError {
2190 if err is ProtocolError {
2191 return err.response_error
2192 }
2193 if err is ResponseError {
2194 return err
2195 }
2196 return ResponseError{
2197 code: internal_error.code
2198 message: err.msg()
2199 }
2200}
2201
2202fn build_sse_response(message string) string {
2203 mut lines := []string{}
2204 for line in message.split('\n') {
2205 lines << 'data: ${line}'
2206 }
2207 return lines.join('\n') + '\n\n'
2208}
2209
2210fn build_sse_event(event LoggedEvent) string {
2211 mut lines := ['id: ${event.id}']
2212 for line in event.body.split('\n') {
2213 lines << 'data: ${line}'
2214 }
2215 return lines.join('\n') + '\n\n'
2216}
2217
2218fn build_sse_stream(events []LoggedEvent) string {
2219 mut chunks := []string{cap: events.len}
2220 for event in events {
2221 chunks << build_sse_event(event)
2222 }
2223 return chunks.join('')
2224}
2225
2226// accept_modes describes which response Content-Types the client tolerates.
2227struct AcceptModes {
2228 json bool
2229 sse bool
2230}
2231
2232// parse_accept extracts JSON/SSE acceptance from a comma-separated Accept header.
2233// `*/*` and `application/*` / `text/*` wildcards count as accepting the matching type.
2234fn parse_accept(header http.Header) AcceptModes {
2235 accept := header.get(.accept) or { '' }
2236 if accept.trim_space() == '' {
2237 return AcceptModes{
2238 json: true
2239 sse: true
2240 }
2241 }
2242 mut modes := AcceptModes{}
2243 for raw in accept.split(',') {
2244 entry := raw.split(';')[0].trim_space().to_lower()
2245 match entry {
2246 '*/*', '' { modes = AcceptModes{true, true} }
2247 'application/json', 'application/*' { modes = AcceptModes{true, modes.sse} }
2248 'text/event-stream', 'text/*' { modes = AcceptModes{modes.json, true} }
2249 else {}
2250 }
2251 }
2252 return modes
2253}
2254
2255// origin_is_allowed enforces the spec MUST: validate Origin to prevent DNS rebinding.
2256fn (s &Server) origin_is_allowed(origin string) bool {
2257 if origin == '' {
2258 return true
2259 }
2260 for allowed in s.allowed_origins {
2261 if allowed == '*' || allowed == origin {
2262 return true
2263 }
2264 }
2265 if s.allowed_origins.len != 0 {
2266 return false
2267 }
2268 return is_loopback_origin(origin)
2269}
2270
2271fn is_loopback_origin(origin string) bool {
2272 if origin == 'null' {
2273 return true
2274 }
2275 mut without_scheme := origin
2276 for prefix in ['http://', 'https://'] {
2277 if origin.starts_with(prefix) {
2278 without_scheme = origin[prefix.len..]
2279 break
2280 }
2281 }
2282 host := without_scheme.all_before('/').all_before(':').to_lower()
2283 bracketed := without_scheme.all_before('/')
2284 return host in ['localhost', '127.0.0.1', '::1'] || bracketed.starts_with('[::1]')
2285}
2286
2287// supports_protocol_version applies spec rules: missing header defaults to
2288// 2025-03-26 (back-compat), otherwise the value MUST match the negotiated one.
2289fn (s &Server) supports_protocol_version(value string, has_session bool) bool {
2290 if value == '' {
2291 return !has_session || s.protocol_version == default_protocol_version
2292 || s.protocol_version == protocol_version
2293 }
2294 return value == s.protocol_version
2295}
2296
2297fn json_http_response(status http.Status, body string, content_type string) http.Response {
2298 mut header := http.new_header()
2299 if content_type != '' {
2300 header.set(.content_type, content_type)
2301 }
2302 mut response := http.Response{
2303 body: body
2304 header: header
2305 }
2306 response.set_status(status)
2307 return response
2308}
2309
2310fn error_http_response(status http.Status, err ResponseError) http.Response {
2311 body := Response{
2312 error: err
2313 }.encode()
2314 return json_http_response(status, body, default_content_type)
2315}
2316
2317struct HttpHandler {
2318mut:
2319 server &Server = unsafe { nil }
2320}
2321
2322fn (mut h HttpHandler) handle(req http.Request) http.Response {
2323 return h.server.handle_http_request(req)
2324}
2325
2326fn (mut s Server) handle_http_get(req http.Request, session_id string) http.Response {
2327 if session_id == '' {
2328 return error_http_response(.bad_request, ResponseError{
2329 code: invalid_request.code
2330 message: 'GET requires an active MCP-Session-Id.'
2331 })
2332 }
2333 if !s.session_exists(session_id) {
2334 return json_http_response(.not_found, '', '')
2335 }
2336 accept := parse_accept(req.header)
2337 if !accept.sse {
2338 return error_http_response(.not_acceptable, ResponseError{
2339 code: invalid_request.code
2340 message: 'GET requires Accept: text/event-stream.'
2341 })
2342 }
2343 last_event_id_text := req.header.get_custom(last_event_id_header) or { '' }
2344 last_event_id := last_event_id_text.int()
2345 mut events := []LoggedEvent{}
2346 if last_event_id_text != '' {
2347 // Flush anything generated while the client was disconnected into the
2348 // event log first, so the replay covers the whole gap. Without this
2349 // step the resume only sees what was already drained before the drop,
2350 // which defeats the point of `Last-Event-ID`.
2351 s.drain_to_event_log(session_id)
2352 events = s.replay_events_after(session_id, last_event_id)
2353 } else {
2354 events = s.drain_to_event_log(session_id)
2355 }
2356 body := build_sse_stream(events)
2357 mut response := json_http_response(.ok, body, event_stream_content_type)
2358 response.header.set_custom(mcp_session_id_header, session_id) or {}
2359 return response
2360}
2361
2362fn (mut s Server) handle_http_request(req http.Request) http.Response {
2363 if req.url.all_before('?') != s.http_path {
2364 return json_http_response(.not_found, '', '')
2365 }
2366 origin := req.header.get_custom('Origin') or { '' }
2367 if !s.origin_is_allowed(origin) {
2368 return error_http_response(.forbidden, ResponseError{
2369 code: invalid_request.code
2370 message: 'Origin not allowed.'
2371 })
2372 }
2373 session_id := req.header.get_custom(mcp_session_id_header) or { '' }
2374 match req.method {
2375 .delete {
2376 if session_id == '' {
2377 return json_http_response(.bad_request, '', '')
2378 }
2379 if !s.delete_session(session_id) {
2380 return json_http_response(.not_found, '', '')
2381 }
2382 return json_http_response(.ok, '', '')
2383 }
2384 .get {
2385 return s.handle_http_get(req, session_id)
2386 }
2387 .post {}
2388 else {
2389 return json_http_response(.method_not_allowed, '', '')
2390 }
2391 }
2392
2393 protocol_value := req.header.get_custom(mcp_protocol_version_header) or { '' }
2394 if !s.supports_protocol_version(protocol_value, session_id != '') {
2395 return error_http_response(.bad_request, ResponseError{
2396 code: invalid_request.code
2397 message: 'Unsupported MCP-Protocol-Version `${protocol_value}`.'
2398 })
2399 }
2400
2401 accept := parse_accept(req.header)
2402 if !accept.json && !accept.sse {
2403 return error_http_response(.not_acceptable, ResponseError{
2404 code: invalid_request.code
2405 message: 'Accept must include application/json or text/event-stream.'
2406 })
2407 }
2408
2409 trimmed := req.data.trim_space()
2410 if trimmed.len == 0 || trimmed[0] == `[` {
2411 return error_http_response(.bad_request, invalid_request)
2412 }
2413 envelope := decode_envelope(trimmed) or {
2414 return error_http_response(.bad_request, parse_error)
2415 }
2416 if session_id != '' && !s.session_exists(session_id) {
2417 return json_http_response(.not_found, '', '')
2418 }
2419 if envelope.method != 'initialize' && envelope.method.len != 0 && session_id == '' {
2420 return error_http_response(.bad_request, server_not_initialized)
2421 }
2422 dispatch_result := s.dispatch_envelope(envelope, session_id, .http) or {
2423 return error_http_response(.bad_request, normalize_response_error(err))
2424 }
2425 if !dispatch_result.has_response {
2426 return json_http_response(.accepted, '', '')
2427 }
2428 use_sse := accept.sse && !accept.json
2429 mut body := ''
2430 mut content_type := default_content_type
2431 if use_sse {
2432 notifications := s.drain_to_event_log(session_for_drain(session_id,
2433 dispatch_result.session_id))
2434 body = build_sse_stream(notifications) + build_sse_response(dispatch_result.response)
2435 content_type = event_stream_content_type
2436 } else {
2437 body = dispatch_result.response
2438 }
2439 mut response := json_http_response(.ok, body, content_type)
2440 if dispatch_result.session_id != '' {
2441 response.header.set_custom(mcp_session_id_header, dispatch_result.session_id) or {}
2442 }
2443 return response
2444}
2445
2446fn session_for_drain(request_session string, dispatch_session string) string {
2447 if request_session != '' {
2448 return request_session
2449 }
2450 return dispatch_session
2451}
2452