module mcp import json import io import net.http import os import rand import sync import time const default_server_name = 'v.mcp.server' const default_server_version = 'dev' const default_http_path = '/mcp' const default_list_page_size = 50 const completion_values_limit = 100 const stdio_session_id = 'stdio' const event_log_capacity = 1024 // SessionTransport identifies how an MCP session is connected. pub enum SessionTransport { stdio http } // LogLevel is the RFC 5424 severity used by `notifications/message`. pub enum LogLevel { debug info notice warning error critical alert emergency } // str returns the wire string for a log level (lowercase per spec). pub fn (l LogLevel) str() string { return match l { .debug { 'debug' } .info { 'info' } .notice { 'notice' } .warning { 'warning' } .error { 'error' } .critical { 'critical' } .alert { 'alert' } .emergency { 'emergency' } } } // parse_log_level decodes the wire string for a log level. pub fn parse_log_level(value string) ?LogLevel { return match value { 'debug' { LogLevel.debug } 'info' { LogLevel.info } 'notice' { LogLevel.notice } 'warning' { LogLevel.warning } 'error' { LogLevel.error } 'critical' { LogLevel.critical } 'alert' { LogLevel.alert } 'emergency' { LogLevel.emergency } else { none } } } // Context provides request-scoped server metadata to handlers. pub struct Context { pub: session_id string @[json: sessionId] request_id string @[json: requestId] method string transport SessionTransport protocol_version string @[json: protocolVersion] client_info Implementation @[json: clientInfo] client_capabilities string @[json: clientCapabilities; raw] progress_token string @[json: progressToken; raw] mut: server &Server = unsafe { nil } @[skip] } // is_cancelled reports whether the client has sent `notifications/cancelled` // for this request. Handlers should poll this for cooperative cancellation. pub fn (ctx Context) is_cancelled() bool { if isnil(ctx.server) { return false } return ctx.server.is_request_cancelled(ctx.session_id, ctx.request_id) } // notify_progress sends `notifications/progress` for this request when the // client included a `_meta.progressToken`. `total` and `message` are optional // (pass 0 / '' to omit). pub fn (ctx Context) notify_progress(progress f64, total f64, message string) { if isnil(ctx.server) || ctx.progress_token.trim_space() == '' { return } ctx.server.notify_progress_for(ctx.session_id, ctx.progress_token, progress, total, message) } // ToolAnnotations exposes the optional behavioural hints described by the // MCP spec (`tools/list` Annotations object). pub struct ToolAnnotations { pub: title string @[omitempty] read_only_hint ?bool @[json: readOnlyHint] destructive_hint ?bool @[json: destructiveHint] idempotent_hint ?bool @[json: idempotentHint] open_world_hint ?bool @[json: openWorldHint] } // ToolExecution carries optional execution metadata for a tool. Set // `task_support` to one of `'forbidden'`, `'optional'` or `'required'` to // advertise tasks/* support; the default (empty) omits the field on the wire. pub struct ToolExecution { pub: task_support string @[json: taskSupport; omitempty] } // Annotations are the optional rendering hints attached to resources, resource // templates and content blocks. `audience` is a list of MCP `Role` values // (e.g. `'user'`, `'assistant'`); `priority` is in the `[0.0, 1.0]` range with // higher meaning more important; `last_modified` is an ISO 8601 timestamp. pub struct Annotations { pub: audience []string @[omitempty] priority ?f64 last_modified string @[json: lastModified; omitempty] } // Tool describes an MCP tool exposed by the server. pub struct Tool { pub: name string title string @[omitempty] description string @[omitempty] input_schema string = default_tool_input_schema @[json: inputSchema; raw] output_schema string @[json: outputSchema; omitempty; raw] annotations ToolAnnotations icons []Icon @[omitempty] execution ToolExecution } // Resource describes a concrete MCP resource exposed by the server. pub struct Resource { pub: uri string name string title string @[omitempty] description string @[omitempty] mime_type string @[json: mimeType; omitempty] size ?int icons []Icon @[omitempty] annotations Annotations } // ResourceTemplate describes a parameterized MCP resource URI template. pub struct ResourceTemplate { pub: uri_template string @[json: uriTemplate] name string title string @[omitempty] description string @[omitempty] mime_type string @[json: mimeType; omitempty] icons []Icon @[omitempty] annotations Annotations } // ResourceContents contains the result of `resources/read`. pub struct ResourceContents { pub: uri string mime_type string @[json: mimeType; omitempty] text string @[omitempty] blob string @[omitempty] } // ReadResourceResult is returned by `resources/read`. pub struct ReadResourceResult { pub: contents []ResourceContents } // PromptArgument describes one prompt argument. pub struct PromptArgument { pub: name string description string @[omitempty] required bool } // Prompt describes an MCP prompt exposed by the server. pub struct Prompt { pub: name string title string @[omitempty] description string @[omitempty] arguments []PromptArgument icons []Icon @[omitempty] } // PromptMessage is one message returned by `prompts/get`. pub struct PromptMessage { pub: role string content string @[raw] } // GetPromptResult is returned by `prompts/get`. pub struct GetPromptResult { pub: description string @[omitempty] messages []PromptMessage } // ToolResult is returned by `tools/call`. pub struct ToolResult { pub: content string @[omitempty; raw] structured_content string @[json: structuredContent; omitempty; raw] is_error bool @[json: isError] } // ToolHandler handles `tools/call` for a registered tool. pub type ToolHandler = fn (ctx Context, arguments string) !ToolResult // ResourceHandler handles `resources/read` for a registered resource URI. pub type ResourceHandler = fn (ctx Context, uri string) !ReadResourceResult // PromptHandler handles `prompts/get` for a registered prompt. pub type PromptHandler = fn (ctx Context, arguments string) !GetPromptResult // CompletionRef identifies the prompt or resource template a completion // targets. Prompt refs set `name`; resource refs set `uri`. pub struct CompletionRef { pub: ref_type string @[json: type] // 'ref/prompt' or 'ref/resource' name string @[omitempty] uri string @[omitempty] } // CompletionResult is the payload returned by `completion/complete`. pub struct CompletionResult { pub: values []string total ?int has_more ?bool } // CompletionHandler returns candidate values for a partial argument value. // `arguments_json` is the JSON object of already-supplied sibling arguments // (the spec's `context.arguments`). pub type CompletionHandler = fn (ctx Context, current_value string, arguments_json string) !CompletionResult // ServerConfig configures an MCP server instance. @[params] pub struct ServerConfig { pub: name string version string title string description string website_url string icons []Icon protocol_version string = protocol_version capabilities string instructions string http_path string = default_http_path // enable_logging declares the `logging` capability and lets clients call // `logging/setLevel`. Disabled by default — turn on when the server emits // `notifications/message` payloads. enable_logging bool // allowed_origins lists the Origin header values accepted on Streamable // HTTP. Use `*` to accept any origin (NOT recommended). When empty the // server only accepts requests without an Origin header or from the // loopback addresses (`http://localhost[:port]`, `http://127.0.0.1[:port]`, // `http://[::1][:port]`, or the literal string `null`). allowed_origins []string } struct RegisteredTool { tool Tool handler ToolHandler = unsafe { nil } } struct RegisteredResource { resource Resource handler ResourceHandler = unsafe { nil } } struct RegisteredPrompt { prompt Prompt handler PromptHandler = unsafe { nil } } struct RegisteredCompletion { ref CompletionRef argument string handler CompletionHandler = unsafe { nil } } // Root identifies a filesystem-or-URI boundary advertised by the client in // response to `roots/list`. pub struct Root { pub: uri string name string @[omitempty] } // ListRootsResult is the typed payload returned by the client to a server // `roots/list` request. pub struct ListRootsResult { pub: roots []Root } // SamplingMessage is one message of a sampling/createMessage exchange. pub struct SamplingMessage { pub: role string content string @[raw] } // ModelHint is a name hint for sampling/createMessage model selection. pub struct ModelHint { pub: name string } // ModelPreferences expresses sampling/createMessage routing weights. pub struct ModelPreferences { pub: hints []ModelHint cost_priority f64 @[json: costPriority; omitempty] speed_priority f64 @[json: speedPriority; omitempty] intelligence_priority f64 @[json: intelligencePriority; omitempty] } // ToolChoice configures `sampling/createMessage` tool-use behaviour. `mode` // is one of `'auto'` (default), `'required'`, or `'none'`. pub struct ToolChoice { pub: mode string } // CreateMessageParams are the typed parameters for sampling/createMessage. pub struct CreateMessageParams { pub: messages []SamplingMessage model_preferences ModelPreferences @[json: modelPreferences] system_prompt string @[json: systemPrompt; omitempty] max_tokens int @[json: maxTokens] temperature f64 @[omitempty] stop_sequences []string @[json: stopSequences; omitempty] metadata string @[omitempty; raw] tools []Tool @[omitempty] tool_choice ToolChoice @[json: toolChoice; omitempty] include_context string @[json: includeContext; omitempty] } // CreateMessageResult is the typed payload returned by the client. pub struct CreateMessageResult { pub: role string content string @[raw] model string stop_reason string @[json: stopReason] } // ElicitSchema is the requested object schema sent to the client for elicitation. pub struct ElicitSchema { pub: type_ string = 'object' @[json: type] properties string @[raw] required []string @[omitempty] } // ElicitParams are the typed parameters for elicitation/create. Set `mode` // to `'url'` and supply `url` + `elicitation_id` to send a URL-mode request; // otherwise the call defaults to form mode and `requested_schema` is // expected to describe the form fields. pub struct ElicitParams { pub: mode string @[omitempty] message string requested_schema ElicitSchema @[json: requestedSchema; omitempty] url string @[omitempty] elicitation_id string @[json: elicitationId; omitempty] } // ElicitResult is the typed payload returned by the client. pub struct ElicitResult { pub: action string content string @[omitempty; raw] } struct LoggedEvent { id int body string } struct Session { mut: id string transport SessionTransport protocol_version string client_info Implementation client_capabilities string initialize_complete bool initialized bool notification_queue []string subscribed_uris []string log_level LogLevel = .debug log_level_set bool cancelled_requests []string event_log []LoggedEvent next_event_id int = 1 next_request_seq int = 1 pending_responses map[string]Response // progress_seen tracks the last progress value emitted for a given // `progressToken`. The MCP spec requires `progress` values to be strictly // monotonically increasing, so we silently drop any non-increasing call // rather than send an out-of-order notification on the wire. progress_seen map[string]f64 } struct ServerState { mut: sessions map[string]Session // response_signals carries one semaphore per in-flight server-initiated // request, keyed by `pending_signal_key(session_id, request_id)`. // `wait_for_response` blocks on it (with a deadline) and // `deliver_response` / `delete_session` post it so we never burn CPU // polling for a response that may take seconds to arrive. Kept outside // `Session` to avoid V's pointer-in-shared-map access warnings. response_signals map[string]&sync.Semaphore } fn pending_signal_key(session_id string, request_id string) string { return '${session_id}\x00${request_id}' } struct DispatchResult { has_response bool response string session_id string } struct HandledRequest { response Response session_id string } struct ProtocolError { response_error ResponseError request_id string } fn (err ProtocolError) msg() string { return err.response_error.message } fn (err ProtocolError) code() int { return err.response_error.code } struct CursorParams { cursor string } struct ToolCallParams { name string arguments string @[raw] } struct ReadResourceParams { uri string } struct SubscribeParams { uri string } struct GetPromptParams { name string arguments string @[raw] } struct ListToolsResult { tools []Tool next_cursor string @[json: nextCursor; omitempty] } struct ListResourcesResult { resources []Resource next_cursor string @[json: nextCursor; omitempty] } struct ListResourceTemplatesResult { resource_templates []ResourceTemplate @[json: resourceTemplates] next_cursor string @[json: nextCursor; omitempty] } struct ListPromptsResult { prompts []Prompt next_cursor string @[json: nextCursor; omitempty] } const default_tool_input_schema = '{"type":"object","additionalProperties":false}' // Server handles MCP protocol requests for stdio and HTTP transports. @[heap] pub struct Server { mut: server_info Implementation protocol_version string capabilities_override string instructions string http_path string allowed_origins []string enable_logging bool http_server &http.Server = unsafe { nil } tools map[string]RegisteredTool tool_names []string resources map[string]RegisteredResource resource_uris []string resource_templates map[string]ResourceTemplate resource_template_ids []string prompts map[string]RegisteredPrompt prompt_names []string completions map[string]RegisteredCompletion state shared ServerState } // new_server constructs a new MCP server. pub fn new_server(config ServerConfig) Server { return Server{ server_info: normalize_server_info(config) protocol_version: normalize_protocol_version(config.protocol_version) capabilities_override: config.capabilities.trim_space() instructions: config.instructions http_path: normalize_http_path(config.http_path) allowed_origins: config.allowed_origins.clone() enable_logging: config.enable_logging tools: map[string]RegisteredTool{} resources: map[string]RegisteredResource{} resource_templates: map[string]ResourceTemplate{} prompts: map[string]RegisteredPrompt{} completions: map[string]RegisteredCompletion{} state: ServerState{} } } // add_tool registers a tool and its handler. pub fn (mut s Server) add_tool(tool Tool, handler ToolHandler) ! { validate_tool_name(tool.name)! if tool.name in s.tools { return error('mcp.Server.add_tool: duplicate tool `${tool.name}`') } normalized := Tool{ name: tool.name title: tool.title description: tool.description input_schema: normalize_tool_input_schema(tool.input_schema) output_schema: tool.output_schema.trim_space() annotations: tool.annotations icons: tool.icons execution: tool.execution } s.tools[tool.name] = RegisteredTool{ tool: normalized handler: handler } s.tool_names << tool.name s.broadcast_notification('notifications/tools/list_changed', empty_object.str()) } // add_resource registers a concrete resource and its read handler. pub fn (mut s Server) add_resource(resource Resource, handler ResourceHandler) ! { if resource.uri.trim_space() == '' { return error('mcp.Server.add_resource: empty uri') } if resource.uri in s.resources { return error('mcp.Server.add_resource: duplicate resource `${resource.uri}`') } s.resources[resource.uri] = RegisteredResource{ resource: resource handler: handler } s.resource_uris << resource.uri s.broadcast_notification('notifications/resources/list_changed', empty_object.str()) } // add_resource_template registers a resource template exposed by `resources/templates/list`. pub fn (mut s Server) add_resource_template(template ResourceTemplate) ! { if template.uri_template.trim_space() == '' { return error('mcp.Server.add_resource_template: empty uri template') } if template.uri_template in s.resource_templates { return error('mcp.Server.add_resource_template: duplicate resource template `${template.uri_template}`') } s.resource_templates[template.uri_template] = template s.resource_template_ids << template.uri_template s.broadcast_notification('notifications/resources/list_changed', empty_object.str()) } // add_prompt registers a prompt and its handler. pub fn (mut s Server) add_prompt(prompt Prompt, handler PromptHandler) ! { if prompt.name.trim_space() == '' { return error('mcp.Server.add_prompt: empty prompt name') } if prompt.name in s.prompts { return error('mcp.Server.add_prompt: duplicate prompt `${prompt.name}`') } s.prompts[prompt.name] = RegisteredPrompt{ prompt: prompt handler: handler } s.prompt_names << prompt.name s.broadcast_notification('notifications/prompts/list_changed', empty_object.str()) } // add_completion registers a completion handler for one argument of a prompt // or resource template. `argument` is the argument name being completed. pub fn (mut s Server) add_completion(ref CompletionRef, argument string, handler CompletionHandler) ! { if argument.trim_space() == '' { return error('mcp.Server.add_completion: empty argument name') } key := completion_key(ref, argument) or { return error('mcp.Server.add_completion: ${err.msg()}') } if key in s.completions { return error('mcp.Server.add_completion: duplicate completion `${key}`') } s.completions[key] = RegisteredCompletion{ ref: ref argument: argument handler: handler } } fn completion_key(ref CompletionRef, argument string) !string { match ref.ref_type { 'ref/prompt' { if ref.name.trim_space() == '' { return error('ref/prompt requires a `name`') } return 'prompt|${ref.name}|${argument}' } 'ref/resource' { if ref.uri.trim_space() == '' { return error('ref/resource requires a `uri`') } return 'resource|${ref.uri}|${argument}' } else { return error('unsupported ref type `${ref.ref_type}`') } } } // notify_tools_list_changed broadcasts the tool catalog change notification. pub fn (mut s Server) notify_tools_list_changed() { s.broadcast_notification('notifications/tools/list_changed', empty_object.str()) } // notify_resources_list_changed broadcasts the resource catalog change notification. pub fn (mut s Server) notify_resources_list_changed() { s.broadcast_notification('notifications/resources/list_changed', empty_object.str()) } // notify_prompts_list_changed broadcasts the prompt catalog change notification. pub fn (mut s Server) notify_prompts_list_changed() { s.broadcast_notification('notifications/prompts/list_changed', empty_object.str()) } // notify_log emits a `notifications/message` payload at `level` filtered per // session by the most recent `logging/setLevel` call. `data_json` MUST be a // valid JSON value (object preferred per spec). `logger` is optional. pub fn (mut s Server) notify_log(level LogLevel, logger string, data_json string) { if !s.enable_logging { return } mut fields := ['"level":${json.encode(level.str())}'] if logger != '' { fields << '"logger":${json.encode(logger)}' } payload := if data_json.trim_space() == '' { 'null' } else { data_json.trim_space() } fields << '"data":${payload}' params := '{${fields.join(',')}}' message := build_notification_message('notifications/message', params) lock s.state { for id in s.state.sessions.keys() { mut session := s.state.sessions[id] if !session.initialized { continue } if session.log_level_set && int(level) < int(session.log_level) { continue } session.notification_queue << message s.state.sessions[id] = session } } } // list_roots issues `roots/list` to the session and waits for the response. pub fn (mut s Server) list_roots(session_id string, timeout time.Duration) !ListRootsResult { response := s.send_server_request(session_id, 'roots/list', empty_object.str(), timeout)! return response.decode_result[ListRootsResult]() } // sample issues `sampling/createMessage` and waits for the LLM result. pub fn (mut s Server) sample(session_id string, params CreateMessageParams, timeout time.Duration) !CreateMessageResult { encoded := json.encode(params) response := s.send_server_request(session_id, 'sampling/createMessage', encoded, timeout)! return response.decode_result[CreateMessageResult]() } // elicit issues `elicitation/create` and waits for the user-supplied content. pub fn (mut s Server) elicit(session_id string, params ElicitParams, timeout time.Duration) !ElicitResult { encoded := encode_elicit_params(params) response := s.send_server_request(session_id, 'elicitation/create', encoded, timeout)! return response.decode_result[ElicitResult]() } fn encode_elicit_params(params ElicitParams) string { mut fields := []string{} if params.mode != '' { fields << '"mode":${json.encode(params.mode)}' } fields << '"message":${json.encode(params.message)}' if params.url != '' { fields << '"url":${json.encode(params.url)}' } if params.elicitation_id != '' { fields << '"elicitationId":${json.encode(params.elicitation_id)}' } if params.requested_schema.properties.trim_space() != '' || params.requested_schema.required.len > 0 { fields << '"requestedSchema":${encode_elicit_schema(params.requested_schema)}' } return '{${fields.join(',')}}' } fn encode_elicit_schema(schema ElicitSchema) string { type_value := if schema.type_.trim_space() == '' { 'object' } else { schema.type_ } mut fields := ['"type":${json.encode(type_value)}'] if schema.properties.trim_space() != '' { fields << '"properties":${schema.properties.trim_space()}' } if schema.required.len > 0 { fields << '"required":[${schema.required.map(json.encode(it)).join(',')}]' } return '{${fields.join(',')}}' } fn (mut s Server) send_server_request(session_id string, method string, params_json string, timeout time.Duration) !Response { request_id_quoted := s.allocate_server_request_id(session_id) or { return error('mcp.Server.send_server_request: unknown session `${session_id}`') } encoded := Request{ id: request_id_quoted method: method params: params_json }.encode() lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] session.notification_queue << encoded s.state.sessions[session_id] = session } } return s.wait_for_response(session_id, request_id_quoted, timeout) } fn (mut s Server) allocate_server_request_id(session_id string) ?string { mut request_id := '' lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] request_id = '"server-${session.next_request_seq}"' session.next_request_seq++ s.state.sessions[session_id] = session s.state.response_signals[pending_signal_key(session_id, request_id)] = sync.new_semaphore() } } if request_id == '' { return none } return request_id } fn (mut s Server) deliver_response(session_id string, response Response) { key := pending_signal_key(session_id, response.id) mut signal := unsafe { &sync.Semaphore(nil) } lock s.state { // Only stash the reply if a waiter is still listening on this id. // `wait_for_response` removes its semaphore entry on timeout, so a // missing signal means the request has been abandoned and any late // reply must be dropped instead of accumulating in `pending_responses`. signal = s.state.response_signals[key] or { unsafe { nil } } if !isnil(signal) && session_id in s.state.sessions { mut session := s.state.sessions[session_id] session.pending_responses[response.id] = response s.state.sessions[session_id] = session } } if !isnil(signal) { signal.post() } } fn (mut s Server) wait_for_response(session_id string, request_id string, timeout time.Duration) !Response { key := pending_signal_key(session_id, request_id) mut signal := unsafe { &sync.Semaphore(nil) } rlock s.state { signal = s.state.response_signals[key] or { unsafe { nil } } } if isnil(signal) { return error('mcp.Server.wait_for_response: no pending request `${request_id}`') } signaled := signal.timed_wait(timeout) mut response := Response{} mut found := false lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] if request_id in session.pending_responses { response = session.pending_responses[request_id] session.pending_responses.delete(request_id) found = true } s.state.sessions[session_id] = session } s.state.response_signals.delete(key) } if found { return response } if !signaled { return error('mcp.Server.wait_for_response: timeout waiting for ${request_id}') } return error('mcp.Server.wait_for_response: session `${session_id}` closed while waiting for ${request_id}') } // notify_elicitation_complete emits a `notifications/elicitation/complete` // notification for the session that initiated a URL-mode elicitation. Pass // the same `elicitation_id` that was supplied to `elicit()` so the client // can correlate the out-of-band interaction with its pending request. pub fn (mut s Server) notify_elicitation_complete(session_id string, elicitation_id string) { params := '{"elicitationId":${json.encode(elicitation_id)}}' message := build_notification_message('notifications/elicitation/complete', params) lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] session.notification_queue << message s.state.sessions[session_id] = session } } } // notify_resource_updated emits notifications/resources/updated to every // session that has subscribed to `uri`. pub fn (mut s Server) notify_resource_updated(uri string) { params := '{"uri":${json.encode(uri)}}' message := build_notification_message('notifications/resources/updated', params) lock s.state { for id in s.state.sessions.keys() { mut session := s.state.sessions[id] if !session.initialized { continue } if uri !in session.subscribed_uris { continue } session.notification_queue << message s.state.sessions[id] = session } } } fn (mut s Server) subscribe(session_id string, uri string) { lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] if uri !in session.subscribed_uris { session.subscribed_uris << uri s.state.sessions[session_id] = session } } } } fn (mut s Server) unsubscribe(session_id string, uri string) { lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] session.subscribed_uris = session.subscribed_uris.filter(it != uri) s.state.sessions[session_id] = session } } } fn build_notification_message(method string, params_json string) string { return Notification{ method: method params: params_json }.encode() } fn (mut s Server) broadcast_notification(method string, params_json string) { message := build_notification_message(method, params_json) lock s.state { for id in s.state.sessions.keys() { mut session := s.state.sessions[id] if !session.initialized { continue } session.notification_queue << message s.state.sessions[id] = session } } } fn (mut s Server) drain_session_notifications(session_id string) []string { mut messages := []string{} lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] messages = session.notification_queue.clone() session.notification_queue = []string{} s.state.sessions[session_id] = session } } return messages } // drain_to_event_log moves queued notifications into the session's bounded // event log and returns the assigned (id, body) pairs ready for SSE framing. fn (mut s Server) drain_to_event_log(session_id string) []LoggedEvent { mut events := []LoggedEvent{} lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] for body in session.notification_queue { event := LoggedEvent{ id: session.next_event_id body: body } session.next_event_id++ session.event_log << event events << event } session.notification_queue = []string{} if session.event_log.len > event_log_capacity { session.event_log = session.event_log[session.event_log.len - event_log_capacity..].clone() } s.state.sessions[session_id] = session } } return events } fn (s &Server) replay_events_after(session_id string, last_event_id int) []LoggedEvent { mut events := []LoggedEvent{} rlock s.state { if session_id in s.state.sessions { session := s.state.sessions[session_id] for event in session.event_log { if event.id > last_event_id { events << event } } } } return events } // serve_stdio starts serving MCP messages over stdio using newline framing. // MCP 2025-11-25 mandates that stdio messages are delimited by newlines and // MUST NOT contain embedded newlines. We bypass libc stdio buffering on the // way in (raw `read()` on fd 0) and flush stdout after every frame on the way // out, so peers behind a pipe see responses immediately and the server // reacts to each line as soon as it arrives. pub fn (mut s Server) serve_stdio() ! { mut stdout := os.stdout() mut source := StdinReader{} mut sink := StdioWriter{ file: &stdout } s.serve_stdio_transport(mut source, mut sink, stdio_session_id, .stdio)! } struct StdinReader {} fn (mut r StdinReader) read(mut buf []u8) !int { if buf.len == 0 { return io.Eof{} } n := unsafe { C.read(0, buf.data, buf.len) } if n == 0 { return io.Eof{} } if n < 0 { return error('mcp.stdio: read(0) failed') } return int(n) } struct StdioWriter { mut: file &os.File = unsafe { nil } } fn (mut w StdioWriter) write(buf []u8) !int { n := w.file.write(buf)! w.file.flush() return n } // serve_http starts serving MCP over a single HTTP endpoint. pub fn (mut s Server) serve_http(addr string) ! { if addr.trim_space() == '' { return error('mcp.Server.serve_http: empty address') } mut handler := HttpHandler{ server: s } mut http_server := &http.Server{ addr: addr handler: handler accept_timeout: 100 * time.millisecond show_startup_message: false } s.http_server = http_server http_server.listen_and_serve() } // close stops the HTTP server if it is running. pub fn (mut s Server) close() { if !isnil(s.http_server) { s.http_server.close() } } // wait_till_running waits until the HTTP server transitions to the running state. pub fn (mut s Server) wait_till_running(params http.WaitTillRunningParams) !int { mut retries := 0 for isnil(s.http_server) && retries < params.max_retries { time.sleep(params.retry_period_ms * time.millisecond) retries++ } if isnil(s.http_server) { return error('mcp.Server.wait_till_running: HTTP server is not running') } remaining_retries := if params.max_retries > retries { params.max_retries - retries } else { 0 } if remaining_retries == 0 { return error('mcp.Server.wait_till_running: HTTP server is not running') } retry_count := s.http_server.wait_till_running( max_retries: remaining_retries retry_period_ms: params.retry_period_ms )! return retries + retry_count } // text_content creates a raw MCP text content item. pub fn text_content(text string) string { return text_content_with_annotations(text, Annotations{}) } // text_content_with_annotations behaves like `text_content` but attaches the // provided `annotations` (audience, priority, lastModified) to the block. pub fn text_content_with_annotations(text string, annotations Annotations) string { mut fields := ['"type":"text"', '"text":${json.encode(text)}'] if encoded := encode_annotations(annotations) { fields << '"annotations":${encoded}' } return '{${fields.join(',')}}' } // image_content creates a raw MCP image content block. `data` must be the // base64-encoded payload and `mime_type` the IANA media type (e.g. `image/png`). pub fn image_content(data string, mime_type string) string { return image_content_with_annotations(data, mime_type, Annotations{}) } // image_content_with_annotations behaves like `image_content` but attaches // the provided `annotations` to the block. pub fn image_content_with_annotations(data string, mime_type string, annotations Annotations) string { mut fields := ['"type":"image"', '"data":${json.encode(data)}', '"mimeType":${json.encode(mime_type)}'] if encoded := encode_annotations(annotations) { fields << '"annotations":${encoded}' } return '{${fields.join(',')}}' } // audio_content creates a raw MCP audio content block. `data` must be the // base64-encoded payload and `mime_type` the IANA media type (e.g. `audio/wav`). pub fn audio_content(data string, mime_type string) string { return audio_content_with_annotations(data, mime_type, Annotations{}) } // audio_content_with_annotations behaves like `audio_content` but attaches // the provided `annotations` to the block. pub fn audio_content_with_annotations(data string, mime_type string, annotations Annotations) string { mut fields := ['"type":"audio"', '"data":${json.encode(data)}', '"mimeType":${json.encode(mime_type)}'] if encoded := encode_annotations(annotations) { fields << '"annotations":${encoded}' } return '{${fields.join(',')}}' } // resource_link_content creates a raw MCP `resource_link` content block from // a `Resource` description. The block carries the resource's metadata — // including any `annotations` set on the resource — so the host can render // or read it later. pub fn resource_link_content(resource Resource) string { encoded := json.encode(resource) if encoded.len <= 2 { return '{"type":"resource_link"}' } return '{"type":"resource_link",${encoded[1..]}' } // embedded_text_resource creates an embedded MCP text resource content block. pub fn embedded_text_resource(uri string, mime_type string, text string) string { return embedded_text_resource_with_annotations(uri, mime_type, text, Annotations{}) } // embedded_text_resource_with_annotations behaves like `embedded_text_resource` // but attaches the provided `annotations` to the outer `EmbeddedResource`. pub fn embedded_text_resource_with_annotations(uri string, mime_type string, text string, annotations Annotations) string { inner := encode_resource_text_payload(uri, mime_type, text) return wrap_embedded_resource(inner, annotations) } // embedded_blob_resource creates an embedded MCP binary resource content // block. `blob` must be the base64-encoded payload. pub fn embedded_blob_resource(uri string, mime_type string, blob string) string { return embedded_blob_resource_with_annotations(uri, mime_type, blob, Annotations{}) } // embedded_blob_resource_with_annotations behaves like `embedded_blob_resource` // but attaches the provided `annotations` to the outer `EmbeddedResource`. pub fn embedded_blob_resource_with_annotations(uri string, mime_type string, blob string, annotations Annotations) string { inner := encode_resource_blob_payload(uri, mime_type, blob) return wrap_embedded_resource(inner, annotations) } fn encode_resource_text_payload(uri string, mime_type string, text string) string { encoded_uri := json.encode(uri) encoded_text := json.encode(text) if mime_type == '' { return '{"uri":${encoded_uri},"text":${encoded_text}}' } encoded_mime := json.encode(mime_type) return '{"uri":${encoded_uri},"mimeType":${encoded_mime},"text":${encoded_text}}' } fn encode_resource_blob_payload(uri string, mime_type string, blob string) string { encoded_uri := json.encode(uri) encoded_blob := json.encode(blob) if mime_type == '' { return '{"uri":${encoded_uri},"blob":${encoded_blob}}' } encoded_mime := json.encode(mime_type) return '{"uri":${encoded_uri},"mimeType":${encoded_mime},"blob":${encoded_blob}}' } fn wrap_embedded_resource(inner string, annotations Annotations) string { mut fields := ['"type":"resource"', '"resource":${inner}'] if encoded := encode_annotations(annotations) { fields << '"annotations":${encoded}' } return '{${fields.join(',')}}' } // encode_annotations renders an `Annotations` value as JSON, omitting the // whole object when no field is populated (returns `none`). fn encode_annotations(annotations Annotations) ?string { mut fields := []string{} if annotations.audience.len > 0 { fields << '"audience":[${annotations.audience.map(json.encode(it)).join(',')}]' } if priority := annotations.priority { fields << '"priority":${format_number(priority)}' } if annotations.last_modified != '' { fields << '"lastModified":${json.encode(annotations.last_modified)}' } if fields.len == 0 { return none } return '{${fields.join(',')}}' } // prompt_text_message creates a prompt message with text content. pub fn prompt_text_message(role string, text string) PromptMessage { return PromptMessage{ role: role content: text_content(text) } } // tool_text_result wraps plain text in an MCP tool result. pub fn tool_text_result(text string) ToolResult { return ToolResult{ content: '[${text_content(text)}]' } } fn tool_error_result(text string) ToolResult { return ToolResult{ content: '[${text_content(text)}]' is_error: true } } fn response_with_json(request_id string, result_json string) Response { return Response{ id: request_id result: result_json } } fn encode_initialize_result(result InitializeResult) string { mut fields := [ '"protocolVersion":${json.encode(result.protocol_version)}', '"capabilities":${normalize_capabilities(result.capabilities)}', '"serverInfo":${json.encode(result.server_info)}', ] if result.instructions != '' { fields << '"instructions":${json.encode(result.instructions)}' } return '{${fields.join(',')}}' } fn encode_tools_list_result(result ListToolsResult) string { mut fields := ['"tools":[${result.tools.map(encode_tool).join(',')}]'] if result.next_cursor != '' { fields << '"nextCursor":${json.encode(result.next_cursor)}' } return '{${fields.join(',')}}' } fn encode_tool(tool Tool) string { mut fields := ['"name":${json.encode(tool.name)}', '"inputSchema":${normalize_tool_input_schema(tool.input_schema)}'] if tool.title != '' { fields << '"title":${json.encode(tool.title)}' } if tool.description != '' { fields << '"description":${json.encode(tool.description)}' } if tool.output_schema.trim_space() != '' { fields << '"outputSchema":${tool.output_schema.trim_space()}' } if encoded_annotations := encode_tool_annotations(tool.annotations) { fields << '"annotations":${encoded_annotations}' } if tool.icons.len > 0 { fields << '"icons":${json.encode(tool.icons)}' } if tool.execution.task_support != '' { fields << '"execution":{"taskSupport":${json.encode(tool.execution.task_support)}}' } return '{${fields.join(',')}}' } fn encode_tool_annotations(annotations ToolAnnotations) ?string { mut fields := []string{} if annotations.title != '' { fields << '"title":${json.encode(annotations.title)}' } if hint := annotations.read_only_hint { fields << '"readOnlyHint":${hint.str()}' } if hint := annotations.destructive_hint { fields << '"destructiveHint":${hint.str()}' } if hint := annotations.idempotent_hint { fields << '"idempotentHint":${hint.str()}' } if hint := annotations.open_world_hint { fields << '"openWorldHint":${hint.str()}' } if fields.len == 0 { return none } return '{${fields.join(',')}}' } fn encode_tool_result(result ToolResult) string { // `content` is REQUIRED by the 2025-11-25 schema (CallToolResult.content), // so always emit at least an empty array; clients reading the result MUST // be able to find `content` whether or not the tool produced output. content_payload := if result.content.trim_space() == '' { '[]' } else { result.content.trim_space() } mut fields := ['"content":${content_payload}', '"isError":${result.is_error.str()}'] if result.structured_content.trim_space() != '' { fields << '"structuredContent":${result.structured_content.trim_space()}' } return '{${fields.join(',')}}' } fn encode_prompt_result(result GetPromptResult) string { mut fields := [ '"messages":[${result.messages.map(encode_prompt_message).join(',')}]', ] if result.description != '' { fields << '"description":${json.encode(result.description)}' } return '{${fields.join(',')}}' } fn encode_prompt_message(message PromptMessage) string { return '{"role":${json.encode(message.role)},"content":${message.content}}' } fn (mut s Server) serve_stdio_transport(mut reader io.Reader, mut writer io.Writer, session_id string, transport SessionTransport) ! { mut buffer := '' for { frame := try_extract_stdio_message(buffer) or { if err.msg() != NoFrameError{}.msg() { error_response := Response{ error: normalize_response_error(err) }.encode() writer.write(encode_stdio_message(error_response).bytes())! return err } FrameExtraction{} } if frame.message.len != 0 { buffer = frame.remaining dispatch_result := s.dispatch_message(frame.message, session_id, transport) or { error_response := Response{ error: normalize_response_error(err) }.encode() writer.write(encode_stdio_message(error_response).bytes())! continue } if dispatch_result.has_response { writer.write(encode_stdio_message(dispatch_result.response).bytes())! } for notification in s.drain_session_notifications(session_id) { writer.write(encode_stdio_message(notification).bytes())! } continue } mut chunk := []u8{len: 4096} bytes_read := reader.read(mut chunk) or { if err is os.Eof { return } if err is io.Eof { return } return err } if bytes_read == 0 { return } buffer += chunk[..bytes_read].bytestr() } } fn (mut s Server) dispatch_message(raw string, session_id string, transport SessionTransport) !DispatchResult { trimmed := raw.trim_space() if trimmed.len == 0 || trimmed[0] == `[` { return ProtocolError{ response_error: invalid_request } } envelope := decode_envelope(trimmed) or { return ProtocolError{ response_error: parse_error } } return s.dispatch_envelope(envelope, session_id, transport) } fn (mut s Server) dispatch_envelope(envelope MessageEnvelope, session_id string, transport SessionTransport) !DispatchResult { if envelope.method.len == 0 { if !is_notification_id(envelope.id) { s.deliver_response(session_id, Response{ id: envelope.id result: envelope.result error: envelope.error }) } return DispatchResult{} } if is_notification_id(envelope.id) { s.handle_notification(Notification{ method: envelope.method params: envelope.params }, session_id, transport)! return DispatchResult{} } req := Request{ id: envelope.id method: envelope.method params: envelope.params } handled := s.handle_request(req, session_id, transport) return DispatchResult{ has_response: true response: handled.response.encode() session_id: handled.session_id } } fn (mut s Server) handle_request(req Request, session_id string, transport SessionTransport) HandledRequest { defer { s.clear_cancelled(session_id, req.id) } return s.handle_request_impl(req, session_id, transport) or { return HandledRequest{ response: error_response_for(req.id, err) } } } fn (mut s Server) handle_request_impl(req Request, session_id string, transport SessionTransport) !HandledRequest { if req.method == 'ping' { return HandledRequest{ response: response_with_json(req.id, empty_object.str()) } } if req.method == 'initialize' { return s.handle_initialize(req, session_id, transport) } session := s.session_for_request(session_id, transport) or { return ProtocolError{ response_error: server_not_initialized request_id: req.id } } if !session.initialize_complete || !session.initialized { return ProtocolError{ response_error: server_not_initialized request_id: req.id } } ctx := s.context_from_session(req, session) match req.method { 'tools/list' { return s.handle_tools_list(req) } 'tools/call' { return s.handle_tools_call(req, ctx) } 'resources/list' { return s.handle_resources_list(req) } 'resources/read' { return s.handle_resources_read(req, ctx) } 'resources/templates/list' { return s.handle_resource_templates_list(req) } 'resources/subscribe' { return s.handle_resources_subscribe(req, session_id) } 'resources/unsubscribe' { return s.handle_resources_unsubscribe(req, session_id) } 'logging/setLevel' { return s.handle_logging_set_level(req, session_id) } 'completion/complete' { return s.handle_completion_complete(req, ctx) } 'prompts/list' { return s.handle_prompts_list(req) } 'prompts/get' { return s.handle_prompts_get(req, ctx) } else { return ProtocolError{ response_error: method_not_found request_id: req.id } } } } fn (mut s Server) handle_initialize(req Request, session_id string, transport SessionTransport) !HandledRequest { params := req.decode_params[InitializeParams]() or { return ProtocolError{ response_error: invalid_params request_id: req.id } } mut session := s.ensure_session_for_initialize(session_id, transport) if session.initialize_complete { return ProtocolError{ response_error: invalid_request request_id: req.id } } // Per MCP lifecycle: server replies with the protocol version it supports; // the client decides whether to proceed or disconnect on a mismatch. session.protocol_version = s.protocol_version session.client_info = normalize_client_info(params.client_info) session.client_capabilities = normalize_capabilities(params.capabilities) session.initialize_complete = true session.initialized = false s.store_session(session) result := InitializeResult{ protocol_version: session.protocol_version capabilities: s.capabilities_json() server_info: s.server_info instructions: s.instructions } return HandledRequest{ response: response_with_json(req.id, encode_initialize_result(result)) session_id: if transport == .http { session.id } else { '' } } } fn (mut s Server) handle_tools_list(req Request) !HandledRequest { params := decode_optional_params[CursorParams](req.params) or { return ProtocolError{ response_error: invalid_params request_id: req.id } } start, end, next_cursor := paginate_bounds(s.tool_names.len, params.cursor)! mut tools := []Tool{cap: end - start} for name in s.tool_names[start..end] { tools << s.tools[name].tool } return HandledRequest{ response: response_with_json(req.id, encode_tools_list_result(ListToolsResult{ tools: tools next_cursor: next_cursor })) } } fn (mut s Server) handle_tools_call(req Request, ctx Context) !HandledRequest { params := decode_optional_params[ToolCallParams](req.params) or { return ProtocolError{ response_error: invalid_params request_id: req.id } } if params.name !in s.tools { return ProtocolError{ response_error: invalid_params request_id: req.id } } entry := s.tools[params.name] result := entry.handler(ctx, json_object_or_empty(params.arguments)) or { if err is ProtocolError { return err } if err is ResponseError { return err } return HandledRequest{ response: response_with_json(req.id, encode_tool_result(tool_error_result(err.msg()))) } } return HandledRequest{ response: response_with_json(req.id, encode_tool_result(result)) } } fn (mut s Server) handle_resources_list(req Request) !HandledRequest { params := decode_optional_params[CursorParams](req.params) or { return ProtocolError{ response_error: invalid_params request_id: req.id } } start, end, next_cursor := paginate_bounds(s.resource_uris.len, params.cursor)! mut resources := []Resource{cap: end - start} for uri in s.resource_uris[start..end] { resources << s.resources[uri].resource } return HandledRequest{ response: response_with_json(req.id, encode_value(ListResourcesResult{ resources: resources next_cursor: next_cursor })) } } fn (mut s Server) handle_resources_read(req Request, ctx Context) !HandledRequest { params := req.decode_params[ReadResourceParams]() or { return ProtocolError{ response_error: invalid_params request_id: req.id } } if params.uri !in s.resources { return ProtocolError{ response_error: ResponseError{ code: resource_not_found.code message: 'Resource not found.' data: '{"uri":${json.encode(params.uri)}}' } request_id: req.id } } entry := s.resources[params.uri] result := entry.handler(ctx, params.uri) or { return err } return HandledRequest{ response: response_with_json(req.id, encode_value(result)) } } struct SetLevelParams { level string } struct CompletionRequestArgument { name string value string } struct CompletionRequestContext { arguments string @[raw] } struct CompletionRequestParams { ref CompletionRef argument CompletionRequestArgument context CompletionRequestContext } fn (mut s Server) handle_completion_complete(req Request, ctx Context) !HandledRequest { if s.completions.len == 0 { return ProtocolError{ response_error: method_not_found request_id: req.id } } params := req.decode_params[CompletionRequestParams]() or { return ProtocolError{ response_error: invalid_params request_id: req.id } } key := completion_key(params.ref, params.argument.name) or { return ProtocolError{ response_error: invalid_params request_id: req.id } } entry := s.completions[key] or { return HandledRequest{ response: response_with_json(req.id, encode_completion_result(CompletionResult{})) } } arguments_json := json_object_or_empty(params.context.arguments) result := entry.handler(ctx, params.argument.value, arguments_json) or { if err is ProtocolError { return err } if err is ResponseError { return err } return ProtocolError{ response_error: ResponseError{ code: internal_error.code message: err.msg() } request_id: req.id } } return HandledRequest{ response: response_with_json(req.id, encode_completion_result(result)) } } fn encode_completion_result(result CompletionResult) string { // MCP 2025-11-25 caps completion responses at 100 values. We clamp here // and surface the truncation through `hasMore: true` so clients keep // requesting refinements instead of silently losing options. clamped := if result.values.len > completion_values_limit { result.values[..completion_values_limit] } else { result.values } mut completion_fields := [ '"values":[${clamped.map(json.encode(it)).join(',')}]', ] if total := result.total { completion_fields << '"total":${total}' } mut has_more_value := result.values.len > completion_values_limit if explicit := result.has_more { has_more_value = explicit || has_more_value } if result.has_more != none || has_more_value { completion_fields << '"hasMore":${has_more_value.str()}' } return '{"completion":{${completion_fields.join(',')}}}' } fn (mut s Server) handle_logging_set_level(req Request, session_id string) !HandledRequest { if !s.enable_logging { return ProtocolError{ response_error: method_not_found request_id: req.id } } params := req.decode_params[SetLevelParams]() or { return ProtocolError{ response_error: invalid_params request_id: req.id } } level := parse_log_level(params.level) or { return ProtocolError{ response_error: invalid_params request_id: req.id } } s.set_session_log_level(session_id, level) return HandledRequest{ response: response_with_json(req.id, empty_object.str()) } } fn (mut s Server) set_session_log_level(session_id string, level LogLevel) { lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] session.log_level = level session.log_level_set = true s.state.sessions[session_id] = session } } } fn (mut s Server) handle_resources_subscribe(req Request, session_id string) !HandledRequest { params := req.decode_params[SubscribeParams]() or { return ProtocolError{ response_error: invalid_params request_id: req.id } } if params.uri.trim_space() == '' { return ProtocolError{ response_error: invalid_params request_id: req.id } } s.subscribe(session_id, params.uri) return HandledRequest{ response: response_with_json(req.id, empty_object.str()) } } fn (mut s Server) handle_resources_unsubscribe(req Request, session_id string) !HandledRequest { params := req.decode_params[SubscribeParams]() or { return ProtocolError{ response_error: invalid_params request_id: req.id } } s.unsubscribe(session_id, params.uri) return HandledRequest{ response: response_with_json(req.id, empty_object.str()) } } fn (mut s Server) handle_resource_templates_list(req Request) !HandledRequest { params := decode_optional_params[CursorParams](req.params) or { return ProtocolError{ response_error: invalid_params request_id: req.id } } start, end, next_cursor := paginate_bounds(s.resource_template_ids.len, params.cursor)! mut templates := []ResourceTemplate{cap: end - start} for uri_template in s.resource_template_ids[start..end] { templates << s.resource_templates[uri_template] } return HandledRequest{ response: response_with_json(req.id, encode_value(ListResourceTemplatesResult{ resource_templates: templates next_cursor: next_cursor })) } } fn (mut s Server) handle_prompts_list(req Request) !HandledRequest { params := decode_optional_params[CursorParams](req.params) or { return ProtocolError{ response_error: invalid_params request_id: req.id } } start, end, next_cursor := paginate_bounds(s.prompt_names.len, params.cursor)! mut prompts := []Prompt{cap: end - start} for name in s.prompt_names[start..end] { prompts << s.prompts[name].prompt } return HandledRequest{ response: response_with_json(req.id, encode_value(ListPromptsResult{ prompts: prompts next_cursor: next_cursor })) } } fn (mut s Server) handle_prompts_get(req Request, ctx Context) !HandledRequest { params := decode_optional_params[GetPromptParams](req.params) or { return ProtocolError{ response_error: invalid_params request_id: req.id } } if params.name !in s.prompts { return ProtocolError{ response_error: invalid_params request_id: req.id } } entry := s.prompts[params.name] result := entry.handler(ctx, json_object_or_empty(params.arguments)) or { return err } return HandledRequest{ response: response_with_json(req.id, encode_prompt_result(result)) } } fn (mut s Server) handle_notification(notification Notification, session_id string, transport SessionTransport) ! { match notification.method { 'notifications/initialized' { mut session := s.session_for_request(session_id, transport) or { return ProtocolError{ response_error: server_not_initialized } } if !session.initialize_complete { return ProtocolError{ response_error: server_not_initialized } } session.initialized = true s.store_session(session) } 'notifications/cancelled' { params := notification.decode_params[CancelledParams]() or { return } s.mark_cancelled(session_id, params.request_id) } else {} } } struct CancelledParams { request_id string @[json: requestId; raw] reason string @[omitempty] } fn (mut s Server) mark_cancelled(session_id string, request_id string) { id := request_id.trim_space() if id.len == 0 { return } lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] if id !in session.cancelled_requests { session.cancelled_requests << id s.state.sessions[session_id] = session } } } } fn (s &Server) is_request_cancelled(session_id string, request_id string) bool { mut cancelled := false rlock s.state { if session_id in s.state.sessions { cancelled = request_id in s.state.sessions[session_id].cancelled_requests } } return cancelled } fn (mut s Server) clear_cancelled(session_id string, request_id string) { lock s.state { if session_id in s.state.sessions { mut session := s.state.sessions[session_id] session.cancelled_requests = session.cancelled_requests.filter(it != request_id) s.state.sessions[session_id] = session } } } fn (s &Server) notify_progress_for(session_id string, progress_token string, progress f64, total f64, message string) { mut fields := [ '"progressToken":${progress_token}', '"progress":${format_number(progress)}', ] if total != 0 { fields << '"total":${format_number(total)}' } if message != '' { fields << '"message":${json.encode(message)}' } params := '{${fields.join(',')}}' notification := build_notification_message('notifications/progress', params) lock s.state { if session_id !in s.state.sessions { return } mut session := s.state.sessions[session_id] // MCP 2025-11-25 mandates `progress` strictly increases per token. if last := session.progress_seen[progress_token] { if progress <= last { return } } session.progress_seen[progress_token] = progress session.notification_queue << notification s.state.sessions[session_id] = session } } fn format_number(value f64) string { if value == f64(i64(value)) { return i64(value).str() } return value.str() } fn (s &Server) capabilities_json() string { if s.capabilities_override != '' { return normalize_capabilities(s.capabilities_override) } mut parts := []string{} if s.tool_names.len != 0 { parts << '"tools":{"listChanged":true}' } if s.resource_uris.len != 0 || s.resource_template_ids.len != 0 { parts << '"resources":{"listChanged":true,"subscribe":true}' } if s.prompt_names.len != 0 { parts << '"prompts":{"listChanged":true}' } if s.enable_logging { parts << '"logging":{}' } if s.completions.len != 0 { parts << '"completions":{}' } return '{${parts.join(',')}}' } fn (mut s Server) ensure_session_for_initialize(session_id string, transport SessionTransport) Session { if transport == .stdio { return s.ensure_session(stdio_session_id, .stdio) } if session_id != '' { return s.ensure_session(session_id, .http) } return s.create_http_session() } fn (mut s Server) ensure_session(session_id string, transport SessionTransport) Session { if session := s.get_session(session_id) { return session } session := Session{ id: session_id transport: transport protocol_version: s.protocol_version } s.store_session(session) return session } fn (mut s Server) create_http_session() Session { for { session_id := rand.uuid_v7() if !s.session_exists(session_id) { session := Session{ id: session_id transport: .http protocol_version: s.protocol_version } s.store_session(session) return session } } return Session{} } fn (s &Server) session_for_request(session_id string, transport SessionTransport) ?Session { if transport == .stdio { return s.get_session(stdio_session_id) } if session_id == '' { return none } return s.get_session(session_id) } fn (mut s Server) store_session(session Session) { lock s.state { s.state.sessions[session.id] = session } } fn (s &Server) get_session(session_id string) ?Session { mut session := Session{} mut found := false rlock s.state { if session_id in s.state.sessions { session = s.state.sessions[session_id] found = true } } if !found { return none } return session } fn (s &Server) session_exists(session_id string) bool { mut found := false rlock s.state { found = session_id in s.state.sessions } return found } fn (mut s Server) delete_session(session_id string) bool { mut deleted := false mut orphaned_signals := []&sync.Semaphore{} prefix := pending_signal_key(session_id, '') lock s.state { if session_id in s.state.sessions { s.state.sessions.delete(session_id) deleted = true } for key, signal in s.state.response_signals { if key.starts_with(prefix) { orphaned_signals << signal } } } for mut signal in orphaned_signals { signal.post() } return deleted } fn (s &Server) context_from_session(req Request, session Session) Context { return Context{ session_id: session.id request_id: req.id method: req.method transport: session.transport protocol_version: session.protocol_version client_info: session.client_info client_capabilities: session.client_capabilities progress_token: extract_progress_token(req.params) server: unsafe { s } } } struct MetaParams { meta MetaPayload @[json: '_meta'] } struct MetaPayload { progress_token string @[json: progressToken; raw] } fn extract_progress_token(params string) string { trimmed := params.trim_space() if trimmed.len == 0 || trimmed == null.str() { return '' } wrapper := json.decode(MetaParams, trimmed) or { return '' } return wrapper.meta.progress_token.trim_space() } fn decode_optional_params[T](raw string) !T { trimmed := raw.trim_space() if trimmed.len == 0 || trimmed == null.str() { return T{} } return decode_value[T](trimmed) } fn json_object_or_empty(raw string) string { trimmed := raw.trim_space() if trimmed.len == 0 || trimmed == null.str() { return '{}' } return trimmed } fn paginate_bounds(total int, cursor string) !(int, int, string) { mut start := 0 if cursor.trim_space() != '' { start = cursor.int() if start < 0 || start > total || (start == 0 && cursor != '0') { return ProtocolError{ response_error: invalid_params } } } mut end := start + default_list_page_size if end > total { end = total } next_cursor := if end < total { end.str() } else { '' } return start, end, next_cursor } fn validate_tool_name(name string) ! { trimmed := name.trim_space() if trimmed.len == 0 || trimmed.len > 128 { return error('mcp.Server.add_tool: invalid tool name `${name}`') } for ch in trimmed { if ch.is_letter() || ch.is_digit() || ch in [`_`, `-`, `.`] { continue } return error('mcp.Server.add_tool: invalid tool name `${name}`') } } fn normalize_tool_input_schema(input_schema string) string { trimmed := input_schema.trim_space() return if trimmed.len == 0 { default_tool_input_schema } else { trimmed } } fn normalize_server_info(config ServerConfig) Implementation { return Implementation{ name: if config.name.trim_space() == '' { default_server_name } else { config.name.trim_space() } version: if config.version.trim_space() == '' { default_server_version } else { config.version.trim_space() } title: config.title.trim_space() description: config.description.trim_space() website_url: config.website_url.trim_space() icons: config.icons.clone() } } fn normalize_http_path(path string) string { trimmed := path.trim_space() if trimmed == '' { return default_http_path } return if trimmed.starts_with('/') { trimmed } else { '/' + trimmed } } fn error_response_for(request_id string, err IError) Response { mut response_id := request_id response_error := normalize_response_error(err) if err is ProtocolError && err.request_id != '' { response_id = err.request_id } return Response{ id: response_id error: response_error } } fn normalize_response_error(err IError) ResponseError { if err is ProtocolError { return err.response_error } if err is ResponseError { return err } return ResponseError{ code: internal_error.code message: err.msg() } } fn build_sse_response(message string) string { mut lines := []string{} for line in message.split('\n') { lines << 'data: ${line}' } return lines.join('\n') + '\n\n' } fn build_sse_event(event LoggedEvent) string { mut lines := ['id: ${event.id}'] for line in event.body.split('\n') { lines << 'data: ${line}' } return lines.join('\n') + '\n\n' } fn build_sse_stream(events []LoggedEvent) string { mut chunks := []string{cap: events.len} for event in events { chunks << build_sse_event(event) } return chunks.join('') } // accept_modes describes which response Content-Types the client tolerates. struct AcceptModes { json bool sse bool } // parse_accept extracts JSON/SSE acceptance from a comma-separated Accept header. // `*/*` and `application/*` / `text/*` wildcards count as accepting the matching type. fn parse_accept(header http.Header) AcceptModes { accept := header.get(.accept) or { '' } if accept.trim_space() == '' { return AcceptModes{ json: true sse: true } } mut modes := AcceptModes{} for raw in accept.split(',') { entry := raw.split(';')[0].trim_space().to_lower() match entry { '*/*', '' { modes = AcceptModes{true, true} } 'application/json', 'application/*' { modes = AcceptModes{true, modes.sse} } 'text/event-stream', 'text/*' { modes = AcceptModes{modes.json, true} } else {} } } return modes } // origin_is_allowed enforces the spec MUST: validate Origin to prevent DNS rebinding. fn (s &Server) origin_is_allowed(origin string) bool { if origin == '' { return true } for allowed in s.allowed_origins { if allowed == '*' || allowed == origin { return true } } if s.allowed_origins.len != 0 { return false } return is_loopback_origin(origin) } fn is_loopback_origin(origin string) bool { if origin == 'null' { return true } mut without_scheme := origin for prefix in ['http://', 'https://'] { if origin.starts_with(prefix) { without_scheme = origin[prefix.len..] break } } host := without_scheme.all_before('/').all_before(':').to_lower() bracketed := without_scheme.all_before('/') return host in ['localhost', '127.0.0.1', '::1'] || bracketed.starts_with('[::1]') } // supports_protocol_version applies spec rules: missing header defaults to // 2025-03-26 (back-compat), otherwise the value MUST match the negotiated one. fn (s &Server) supports_protocol_version(value string, has_session bool) bool { if value == '' { return !has_session || s.protocol_version == default_protocol_version || s.protocol_version == protocol_version } return value == s.protocol_version } fn json_http_response(status http.Status, body string, content_type string) http.Response { mut header := http.new_header() if content_type != '' { header.set(.content_type, content_type) } mut response := http.Response{ body: body header: header } response.set_status(status) return response } fn error_http_response(status http.Status, err ResponseError) http.Response { body := Response{ error: err }.encode() return json_http_response(status, body, default_content_type) } struct HttpHandler { mut: server &Server = unsafe { nil } } fn (mut h HttpHandler) handle(req http.Request) http.Response { return h.server.handle_http_request(req) } fn (mut s Server) handle_http_get(req http.Request, session_id string) http.Response { if session_id == '' { return error_http_response(.bad_request, ResponseError{ code: invalid_request.code message: 'GET requires an active MCP-Session-Id.' }) } if !s.session_exists(session_id) { return json_http_response(.not_found, '', '') } accept := parse_accept(req.header) if !accept.sse { return error_http_response(.not_acceptable, ResponseError{ code: invalid_request.code message: 'GET requires Accept: text/event-stream.' }) } last_event_id_text := req.header.get_custom(last_event_id_header) or { '' } last_event_id := last_event_id_text.int() mut events := []LoggedEvent{} if last_event_id_text != '' { // Flush anything generated while the client was disconnected into the // event log first, so the replay covers the whole gap. Without this // step the resume only sees what was already drained before the drop, // which defeats the point of `Last-Event-ID`. s.drain_to_event_log(session_id) events = s.replay_events_after(session_id, last_event_id) } else { events = s.drain_to_event_log(session_id) } body := build_sse_stream(events) mut response := json_http_response(.ok, body, event_stream_content_type) response.header.set_custom(mcp_session_id_header, session_id) or {} return response } fn (mut s Server) handle_http_request(req http.Request) http.Response { if req.url.all_before('?') != s.http_path { return json_http_response(.not_found, '', '') } origin := req.header.get_custom('Origin') or { '' } if !s.origin_is_allowed(origin) { return error_http_response(.forbidden, ResponseError{ code: invalid_request.code message: 'Origin not allowed.' }) } session_id := req.header.get_custom(mcp_session_id_header) or { '' } match req.method { .delete { if session_id == '' { return json_http_response(.bad_request, '', '') } if !s.delete_session(session_id) { return json_http_response(.not_found, '', '') } return json_http_response(.ok, '', '') } .get { return s.handle_http_get(req, session_id) } .post {} else { return json_http_response(.method_not_allowed, '', '') } } protocol_value := req.header.get_custom(mcp_protocol_version_header) or { '' } if !s.supports_protocol_version(protocol_value, session_id != '') { return error_http_response(.bad_request, ResponseError{ code: invalid_request.code message: 'Unsupported MCP-Protocol-Version `${protocol_value}`.' }) } accept := parse_accept(req.header) if !accept.json && !accept.sse { return error_http_response(.not_acceptable, ResponseError{ code: invalid_request.code message: 'Accept must include application/json or text/event-stream.' }) } trimmed := req.data.trim_space() if trimmed.len == 0 || trimmed[0] == `[` { return error_http_response(.bad_request, invalid_request) } envelope := decode_envelope(trimmed) or { return error_http_response(.bad_request, parse_error) } if session_id != '' && !s.session_exists(session_id) { return json_http_response(.not_found, '', '') } if envelope.method != 'initialize' && envelope.method.len != 0 && session_id == '' { return error_http_response(.bad_request, server_not_initialized) } dispatch_result := s.dispatch_envelope(envelope, session_id, .http) or { return error_http_response(.bad_request, normalize_response_error(err)) } if !dispatch_result.has_response { return json_http_response(.accepted, '', '') } use_sse := accept.sse && !accept.json mut body := '' mut content_type := default_content_type if use_sse { notifications := s.drain_to_event_log(session_for_drain(session_id, dispatch_result.session_id)) body = build_sse_stream(notifications) + build_sse_response(dispatch_result.response) content_type = event_stream_content_type } else { body = dispatch_result.response } mut response := json_http_response(.ok, body, content_type) if dispatch_result.session_id != '' { response.header.set_custom(mcp_session_id_header, dispatch_result.session_id) or {} } return response } fn session_for_drain(request_session string, dispatch_session string) string { if request_session != '' { return request_session } return dispatch_session }