Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+911
View File
@@ -0,0 +1,911 @@
module mcp
import json
import net.http
import os
import time
pub const jsonrpc_version = '2.0'
pub const protocol_version = '2025-11-25'
pub const parse_error = ResponseError{
code: -32700
message: 'Invalid JSON.'
}
pub const invalid_request = ResponseError{
code: -32600
message: 'Invalid request.'
}
pub const method_not_found = ResponseError{
code: -32601
message: 'Method not found.'
}
pub const invalid_params = ResponseError{
code: -32602
message: 'Invalid params.'
}
pub const internal_error = ResponseError{
code: -32603
message: 'Internal error.'
}
pub const server_not_initialized = ResponseError{
code: -32002
message: 'Server not initialized.'
}
pub const resource_not_found = ResponseError{
code: -32002
message: 'Resource not found.'
}
pub const url_elicitation_required = ResponseError{
code: -32042
message: 'URL mode elicitation required.'
}
const default_content_type = 'application/json'
const event_stream_content_type = 'text/event-stream'
const streamable_http_accept = '${default_content_type}, ${event_stream_content_type}'
const mcp_session_id_header = 'MCP-Session-Id'
const mcp_protocol_version_header = 'MCP-Protocol-Version'
const last_event_id_header = 'Last-Event-ID'
const default_protocol_version = '2025-03-26'
const process_poll_interval = 5 * time.millisecond
const default_client_name = 'v.mcp'
const default_client_version = 'dev'
// ResponseError is the JSON-RPC error payload used by MCP responses.
pub struct ResponseError {
pub:
code int
message string
data string @[raw]
}
// code returns the JSON-RPC error code.
pub fn (err ResponseError) code() int {
return err.code
}
// msg returns the JSON-RPC error message.
pub fn (err ResponseError) msg() string {
return err.message
}
// err casts the response error to `IError`.
pub fn (err ResponseError) err() IError {
return IError(err)
}
// Null represents the JSON `null` literal.
pub struct Null {}
// str returns the JSON `null` literal.
pub fn (n Null) str() string {
return 'null'
}
pub const null = Null{}
// Empty omits a JSON-RPC field when used with MCP helpers.
pub struct Empty {}
// str returns the empty string.
pub fn (e Empty) str() string {
return ''
}
pub const empty = Empty{}
// EmptyObject encodes to an empty JSON object.
pub struct EmptyObject {}
// str returns the JSON empty object literal.
pub fn (e EmptyObject) str() string {
return '{}'
}
pub const empty_object = EmptyObject{}
// Icon describes a UI icon advertised by an MCP implementation, tool,
// resource, resource template or prompt. `src` is required; `mime_type`,
// `sizes` and `theme` are optional metadata mirroring the spec's `Icon` shape.
pub struct Icon {
pub:
src string
mime_type string @[json: mimeType; omitempty]
sizes []string @[omitempty]
theme string @[omitempty]
}
// Implementation identifies an MCP client or server implementation. `name`
// and `version` are required; `title`, `description`, `website_url` and
// `icons` are optional 2025-11-25 metadata extensions (BaseMetadata + Icons).
pub struct Implementation {
pub:
name string
version string
title string @[omitempty]
description string @[omitempty]
website_url string @[json: websiteUrl; omitempty]
icons []Icon @[omitempty]
}
// InitializeParams is the typed payload for the `initialize` request.
pub struct InitializeParams {
pub:
protocol_version string @[json: protocolVersion]
capabilities string @[raw]
client_info Implementation @[json: clientInfo]
}
// InitializeResult is the typed result returned by an MCP server after initialization.
pub struct InitializeResult {
pub:
protocol_version string @[json: protocolVersion]
capabilities string @[raw]
server_info Implementation @[json: serverInfo]
instructions string
}
// Request is a JSON-RPC request message encoded for MCP.
pub struct Request {
pub:
jsonrpc string = jsonrpc_version
id string @[raw]
method string
params string @[omitempty; raw]
}
// new_request constructs an MCP request with a typed id and params payload.
pub fn new_request[I, P](id I, method string, params P) Request {
return Request{
id: encode_id(id)
method: method
params: encode_value(params)
}
}
// encode serializes the request to JSON.
pub fn (req Request) encode() string {
params_payload := if req.params.len == 0 { '' } else { ',"params":${req.params}' }
id_payload := if req.id.len == 0 { null.str() } else { req.id }
return '{"jsonrpc":"${jsonrpc_version}","id":${id_payload},"method":${json.encode(req.method)}${params_payload}}'
}
// decode_params decodes the raw request params into `T`.
pub fn (req Request) decode_params[T]() !T {
return decode_value[T](req.params)
}
// Notification is a JSON-RPC notification encoded for MCP.
pub struct Notification {
pub:
jsonrpc string = jsonrpc_version
method string
params string @[omitempty; raw]
}
// new_notification constructs an MCP notification with a typed params payload.
pub fn new_notification[P](method string, params P) Notification {
return Notification{
method: method
params: encode_value(params)
}
}
// encode serializes the notification to JSON.
pub fn (notification Notification) encode() string {
params_payload := if notification.params.len == 0 {
''
} else {
',"params":${notification.params}'
}
return '{"jsonrpc":"${jsonrpc_version}","method":${json.encode(notification.method)}${params_payload}}'
}
// decode_params decodes the raw notification params into `T`.
pub fn (notification Notification) decode_params[T]() !T {
return decode_value[T](notification.params)
}
// Response is a JSON-RPC response message encoded for MCP.
pub struct Response {
pub:
jsonrpc string = jsonrpc_version
id string @[raw]
result string @[raw]
error ResponseError
}
// new_response constructs an MCP response with a typed id and result payload.
pub fn new_response[I, R](id I, result R, err ResponseError) Response {
return Response{
id: encode_id(id)
result: if err.code != 0 { '' } else { encode_value(result) }
error: err
}
}
// encode serializes the response to JSON.
pub fn (resp Response) encode() string {
mut payload := '{"jsonrpc":"${jsonrpc_version}"'
if resp.error.code != 0 {
payload += ',"error":' + encode_response_error(resp.error)
} else {
result_payload := if resp.result.len == 0 { null.str() } else { resp.result }
payload += ',"result":' + result_payload
}
id_payload := if resp.id.len == 0 { null.str() } else { resp.id }
return payload + ',"id":${id_payload}}'
}
// encode_response_error renders a ResponseError as JSON, preserving the
// `data` payload as raw JSON. V's `json.encode` ignores the `@[raw]` tag on
// encode, so a hand-rolled writer is required to keep the wire shape spec
// compliant (the `data` field MAY be any JSON value per JSON-RPC 2.0).
fn encode_response_error(err ResponseError) string {
mut fields := ['"code":${err.code}', '"message":${json.encode(err.message)}']
if err.data.trim_space() != '' {
fields << '"data":${err.data}'
}
return '{${fields.join(',')}}'
}
// decode_result decodes the response result into `T`.
pub fn (resp Response) decode_result[T]() !T {
if resp.error.code != 0 {
return resp.error.err()
}
return decode_value[T](resp.result)
}
// decode_request decodes a JSON payload into an MCP request.
pub fn decode_request(raw string) !Request {
return json.decode(Request, raw) or { return err }
}
// decode_notification decodes a JSON payload into an MCP notification.
pub fn decode_notification(raw string) !Notification {
return json.decode(Notification, raw) or { return err }
}
// decode_response decodes a JSON payload into an MCP response.
pub fn decode_response(raw string) !Response {
return json.decode(Response, raw) or { return err }
}
struct MessageEnvelope {
jsonrpc string
id string @[raw]
method string
params string @[raw]
result string @[raw]
error ResponseError
}
// is_notification_id reports whether a JSON-RPC `id` field encodes the
// "absent or null" form, which per spec marks the envelope as a notification.
fn is_notification_id(id string) bool {
return id == '' || id == null.str()
}
fn (env MessageEnvelope) encode() string {
if env.method.len != 0 {
if is_notification_id(env.id) {
return Notification{
method: env.method
params: env.params
}.encode()
}
return Request{
id: env.id
method: env.method
params: env.params
}.encode()
}
return Response{
id: env.id
result: env.result
error: env.error
}.encode()
}
fn decode_envelope(raw string) !MessageEnvelope {
return json.decode(MessageEnvelope, raw) or { return err }
}
// Transport is the boundary between MCP messages and the wire format.
pub interface Transport {
mut:
send(message string) !
receive() !string
close()
}
@[params]
pub struct ClientConfig {
pub mut:
protocol_version string = protocol_version
client_info Implementation = Implementation{
name: default_client_name
version: default_client_version
}
capabilities string = '{}'
headers map[string]string
}
pub struct Client {
mut:
transport Transport
config ClientConfig
next_id int = 1
initialized bool
init_result InitializeResult
pending_responses map[string]Response
notifications []Notification
server_requests []Request
}
// new_client constructs an MCP client on top of a custom transport.
pub fn new_client(transport Transport, config ClientConfig) Client {
return Client{
transport: transport
config: config
pending_responses: map[string]Response{}
}
}
// connect creates an MCP client for a streamable HTTP endpoint.
pub fn connect(url string) !Client {
return connect_http(url, ClientConfig{})
}
// connect_http creates an MCP client for a streamable HTTP endpoint.
pub fn connect_http(url string, config ClientConfig) !Client {
transport := new_http_transport(url, config)!
return new_client(transport, config)
}
// connect_stdio creates an MCP client that talks to a local stdio server process.
pub fn connect_stdio(command string, args []string, config ClientConfig) !Client {
transport := new_process_transport(command, args)!
return new_client(transport, config)
}
// initialize starts the MCP initialization handshake using the client's config.
pub fn (mut c Client) initialize() !InitializeResult {
return c.initialize_with_raw(c.config.capabilities, c.config.client_info)
}
// initialize_with starts the MCP initialization handshake using typed capabilities.
pub fn (mut c Client) initialize_with[X](capabilities X, client_info Implementation) !InitializeResult {
return c.initialize_with_raw(encode_value(capabilities), client_info)
}
// send_request sends a typed request and waits for its response.
pub fn (mut c Client) send_request(request Request) !Response {
if request.method == 'initialize' {
return error('mcp.Client.initialize must be used for the MCP handshake')
}
c.ensure_initialized()!
c.transport.send(request.encode())!
return c.wait_for_response(request.id)
}
// request_message sends a method call and returns the raw MCP response.
pub fn (mut c Client) request_message[P](method string, params P) !Response {
request := new_request(c.next_request_id(), method, params)
return c.send_request(request)
}
// request sends a method call and decodes its result into `Result`.
pub fn (mut c Client) request[P, R](method string, params P) !R {
response := c.request_message(method, params)!
result := response.decode_result[R]()!
return result
}
// send_notification sends a typed notification message.
pub fn (mut c Client) send_notification(notification Notification) ! {
if notification.method == 'notifications/initialized' {
return error('notifications/initialized is sent automatically after initialization')
}
c.ensure_initialized()!
c.transport.send(notification.encode())!
}
// notify sends a method notification with a typed params payload.
pub fn (mut c Client) notify[P](method string, params P) ! {
c.send_notification(new_notification(method, params))!
}
// take_notifications drains notifications queued while waiting for responses.
pub fn (mut c Client) take_notifications() []Notification {
if c.notifications.len == 0 {
return []Notification{}
}
drained := c.notifications.clone()
c.notifications = []Notification{}
return drained
}
// take_requests drains server initiated requests queued while waiting for responses.
pub fn (mut c Client) take_requests() []Request {
if c.server_requests.len == 0 {
return []Request{}
}
drained := c.server_requests.clone()
c.server_requests = []Request{}
return drained
}
// close releases the underlying transport.
pub fn (mut c Client) close() {
c.transport.close()
}
fn (mut c Client) initialize_with_raw(capabilities string, client_info Implementation) !InitializeResult {
if c.initialized {
return c.init_result
}
c.config.capabilities = normalize_capabilities(capabilities)
c.config.client_info = normalize_client_info(client_info)
params := InitializeParams{
protocol_version: normalize_protocol_version(c.config.protocol_version)
capabilities: c.config.capabilities
client_info: c.config.client_info
}
request := Request{
id: encode_id(c.next_request_id())
method: 'initialize'
params: encode_initialize_params(params)
}
c.transport.send(request.encode())!
response := c.wait_for_response(request.id)!
result := response.decode_result[InitializeResult]()!
c.transport.send(new_notification('notifications/initialized', empty).encode())!
c.initialized = true
c.init_result = result
return result
}
fn (mut c Client) ensure_initialized() ! {
if !c.initialized {
c.initialize()!
}
}
fn (mut c Client) next_request_id() int {
request_id := c.next_id
c.next_id++
return request_id
}
fn (mut c Client) wait_for_response(expected_id string) !Response {
if expected_id in c.pending_responses {
response := c.pending_responses[expected_id]
c.pending_responses.delete(expected_id)
return response
}
for {
raw_message := c.transport.receive()!
envelope := decode_envelope(raw_message)!
if envelope.method.len != 0 {
if is_notification_id(envelope.id) {
c.notifications << Notification{
method: envelope.method
params: envelope.params
}
} else {
c.server_requests << Request{
id: envelope.id
method: envelope.method
params: envelope.params
}
}
continue
}
response := Response{
id: envelope.id
result: envelope.result
error: envelope.error
}
if response.id == expected_id {
return response
}
c.pending_responses[response.id] = response
}
return error('mcp: response loop exited unexpectedly')
}
fn encode_initialize_params(params InitializeParams) string {
return '{"protocolVersion":${json.encode(params.protocol_version)},"capabilities":${normalize_capabilities(params.capabilities)},"clientInfo":${json.encode(params.client_info)}}'
}
struct NoFrameError {}
fn (err NoFrameError) msg() string {
return 'no complete frame available'
}
fn (err NoFrameError) code() int {
return 0
}
// FrameExtraction holds a single stdio message and the unconsumed buffer remainder.
struct FrameExtraction {
message string
remaining string
}
struct HttpTransport {
mut:
url string
header http.Header
session_id string
protocol_version string
pending []string
}
fn new_http_transport(url string, config ClientConfig) !HttpTransport {
if url == '' {
return error('mcp.connect_http: empty url')
}
if !url.starts_with('http://') && !url.starts_with('https://') {
return error('mcp.connect_http: expected an http:// or https:// MCP endpoint')
}
mut header := http.new_header()
header.set(.user_agent, default_client_name)
if config.headers.len != 0 {
header.add_custom_map(config.headers)!
}
return HttpTransport{
url: url
header: header
}
}
fn (mut transport HttpTransport) send(message string) ! {
mut header := transport.header
header.set(.content_type, default_content_type)
header.set(.accept, streamable_http_accept)
if transport.session_id != '' {
header.set_custom(mcp_session_id_header, transport.session_id)!
}
if transport.protocol_version != '' {
header.set_custom(mcp_protocol_version_header, transport.protocol_version)!
}
response := http.fetch(
method: .post
url: transport.url
data: message
header: header
)!
if session_id := response.header.get_custom(mcp_session_id_header) {
transport.session_id = session_id
}
if transport.protocol_version == '' && transport.session_id != '' {
// First handshake response: capture the negotiated version so all
// subsequent requests carry MCP-Protocol-Version per spec §Transports.
transport.protocol_version = read_negotiated_version(response.body)
}
messages := parse_http_response_messages(response)!
if messages.len != 0 {
transport.pending << messages
return
}
if response.status_code >= 400 {
return error('mcp.http: server returned HTTP ${response.status_code} without an MCP payload')
}
}
fn read_negotiated_version(body string) string {
envelope := decode_envelope(body) or { return '' }
result := json.decode(InitializeResult, envelope.result) or { return '' }
return result.protocol_version
}
fn (mut transport HttpTransport) receive() !string {
if transport.pending.len == 0 {
return error('mcp.http: no pending messages are available')
}
message := transport.pending[0]
transport.pending = if transport.pending.len == 1 {
[]string{}
} else {
transport.pending[1..].clone()
}
return message
}
fn (mut transport HttpTransport) close() {
if transport.session_id == '' {
return
}
mut header := transport.header
header.set_custom(mcp_session_id_header, transport.session_id) or { return }
http.fetch(
method: .delete
url: transport.url
header: header
) or {}
transport.session_id = ''
}
struct ProcessTransport {
mut:
process &os.Process
buffer string
}
fn new_process_transport(command string, args []string) !ProcessTransport {
if command == '' {
return error('mcp.connect_stdio: empty command')
}
mut process := os.new_process(command)
process.set_args(args)
process.set_redirect_stdio()
process.run()
return ProcessTransport{
process: process
}
}
fn (mut transport ProcessTransport) send(message string) ! {
transport.process.stdin_write(encode_stdio_message(message))
}
fn (mut transport ProcessTransport) receive() !string {
for {
frame := try_extract_stdio_message(transport.buffer) or {
if err.msg() != NoFrameError{}.msg() {
return err
}
FrameExtraction{}
}
if frame.message.len != 0 {
transport.buffer = frame.remaining
return frame.message
}
if transport.process.is_pending(.stdout) {
chunk := transport.process.stdout_read()
if chunk.len != 0 {
transport.buffer += chunk
continue
}
}
if !transport.process.is_alive() {
transport.buffer += transport.process.stdout_slurp()
frame_after_exit := try_extract_stdio_message(transport.buffer) or {
if err.msg() != NoFrameError{}.msg() {
return err
}
FrameExtraction{}
}
if frame_after_exit.message.len != 0 {
transport.buffer = frame_after_exit.remaining
return frame_after_exit.message
}
stderr_output := transport.process.stderr_slurp().trim_space()
if stderr_output.len != 0 {
return error('mcp.stdio: process exited before a full MCP message was received: ${stderr_output}')
}
return error('mcp.stdio: process exited before a full MCP message was received')
}
time.sleep(process_poll_interval)
}
return error('mcp.stdio: receive loop exited unexpectedly')
}
fn (mut transport ProcessTransport) close() {
if transport.process.is_alive() {
transport.process.signal_term()
for _ in 0 .. 20 {
if !transport.process.is_alive() {
break
}
time.sleep(10 * time.millisecond)
}
if transport.process.is_alive() {
transport.process.signal_kill()
} else if transport.process.status in [.running, .stopped] {
transport.process.wait()
}
}
transport.process.close()
}
fn parse_http_response_messages(response http.Response) ![]string {
content_type := response.header.get(.content_type) or { '' }
body := response.body.trim_space()
if body.len == 0 {
return []string{}
}
content_type_lower := content_type.to_lower()
if content_type_lower.starts_with('application/json')
|| (content_type == '' && is_json_payload(body)) {
return split_json_payloads(body)
}
if content_type_lower.starts_with('text/event-stream') {
return parse_sse_messages(body)
}
return error('mcp.http: unsupported content type `${content_type}`')
}
fn split_json_payloads(body string) ![]string {
trimmed := body.trim_space()
if trimmed.len == 0 {
return []string{}
}
if trimmed[0] != `[` {
return [trimmed]
}
envelopes := json.decode([]MessageEnvelope, trimmed) or { return err }
mut messages := []string{cap: envelopes.len}
for envelope in envelopes {
messages << envelope.encode()
}
return messages
}
fn parse_sse_messages(body string) ![]string {
normalized := body.replace('\r\n', '\n').replace('\r', '\n')
mut data_lines := []string{}
mut messages := []string{}
for line in normalized.split('\n') {
if line.len == 0 {
if data_lines.len != 0 {
append_sse_payload(mut messages, data_lines.join('\n'))!
data_lines = []string{}
}
continue
}
if line.starts_with(':') {
continue
}
if line.starts_with('data:') {
mut payload := line[5..]
if payload.len != 0 && payload[0] == ` ` {
payload = payload[1..]
}
data_lines << payload
}
}
if data_lines.len != 0 {
append_sse_payload(mut messages, data_lines.join('\n'))!
}
return messages
}
fn append_sse_payload(mut messages []string, payload string) ! {
trimmed := payload.trim_space()
if !is_json_payload(trimmed) {
return
}
payloads := split_json_payloads(trimmed)!
for item in payloads {
messages << item
}
}
fn is_json_payload(payload string) bool {
trimmed := payload.trim_space()
if trimmed.len == 0 {
return false
}
return trimmed[0] == `{` || trimmed[0] == `[`
}
// encode_stdio_message produces a newline-delimited stdio frame per MCP spec.
// The MCP spec mandates that messages are delimited by newlines and MUST NOT
// contain embedded newlines. Compact JSON encoding satisfies the latter; we
// strip stray CR/LF defensively to keep the on-wire contract.
fn encode_stdio_message(message string) string {
return message.replace('\r', '').replace('\n', '') + '\n'
}
// try_extract_stdio_message consumes the next newline-delimited frame from
// `buffer`, returning the message body without its trailing newline.
fn try_extract_stdio_message(buffer string) !FrameExtraction {
newline := buffer.index('\n') or { return NoFrameError{} }
mut end := newline
if end > 0 && buffer[end - 1] == `\r` {
end--
}
message := buffer[..end].trim_space()
remaining := if newline + 1 >= buffer.len { '' } else { buffer[newline + 1..] }
if message.len == 0 {
return try_extract_stdio_message(remaining) or {
if err.msg() == NoFrameError{}.msg() {
return NoFrameError{}
}
err
}
}
return FrameExtraction{
message: message
remaining: remaining
}
}
fn encode_id[I](id I) string {
return $if I is int {
id.str()
} $else {
json.encode(id)
}
}
fn encode_value[T](value T) string {
return $if T is Empty {
value.str()
} $else $if T is EmptyObject {
value.str()
} $else $if T is Null {
value.str()
} $else {
json.encode(value)
}
}
fn decode_value[T](value string) !T {
$if T is Empty {
if value == '' || value == null.str() {
return Empty{}
}
return error('mcp: expected an empty payload, got `${value}`')
} $else $if T is EmptyObject {
if value == '{}' {
return EmptyObject{}
}
return error('mcp: expected an empty object payload, got `${value}`')
} $else $if T is Null {
if value == null.str() {
return null
}
return error('mcp: expected null, got `${value}`')
} $else $if T is string {
if value.len >= 2 && value[0] == `"` && value[value.len - 1] == `"` {
return json.decode(string, value) or { return err }
}
return error('mcp: could not decode `${value}` into string')
} $else $if T is bool {
if value == 'true' {
return true
}
if value == 'false' {
return false
}
return error('mcp: could not decode `${value}` into bool')
} $else {
return json.decode(T, value) or { return err }
}
}
fn normalize_client_info(client_info Implementation) Implementation {
return if client_info.name == '' {
Implementation{
name: default_client_name
version: if client_info.version == '' {
default_client_version
} else {
client_info.version
}
}
} else if client_info.version == '' {
Implementation{
name: client_info.name
version: default_client_version
}
} else {
client_info
}
}
fn normalize_capabilities(capabilities string) string {
trimmed := capabilities.trim_space()
return if trimmed.len == 0 { '{}' } else { trimmed }
}
fn normalize_protocol_version(version string) string {
trimmed := version.trim_space()
return if trimmed.len == 0 { protocol_version } else { trimmed }
}