This commit is contained in:
2025-09-14 17:57:06 +02:00
parent 12a00dbc78
commit 5155ab16af
157 changed files with 4 additions and 4 deletions

68
lib/ai/mcpcore/README.md Normal file
View File

@@ -0,0 +1,68 @@
# Model Context Protocol (MCP) Implementation
This module provides a V language implementation of the [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/specification/2024-11-05/) specification. MCP is a protocol designed to standardize communication between AI models and their context providers.
## Overview
The MCP module serves as a core library for building MCP-compliant servers in V. Its main purpose is to provide all the boilerplate MCP functionality, so implementers only need to define and register their specific handlers. The module handles the Standard Input/Output (stdio) transport as described in the [MCP transport specification](https://modelcontextprotocol.io/docs/concepts/transports), enabling standardized communication between AI models and their context providers.
The module implements all the required MCP protocol methods (resources/list, tools/list, prompts/list, etc.) and manages the underlying JSON-RPC communication, allowing developers to focus solely on implementing their specific tools and handlers. The module itself is not a standalone server but rather a framework that can be used to build different MCP server implementations. The subdirectories within this module (such as `baobab` and `developer`) contain specific implementations of MCP servers that utilize this core framework.
## to test
```
curl -fsSL https://bun.sh/install | bash
source /root/.bashrc
```
## Key Components
- **Server**: The main MCP server struct that handles JSON-RPC requests and responses
- **Backend Interface**: Abstraction for different backend implementations (memory-based by default)
- **Model Configuration**: Structures representing client and server capabilities according to the MCP specification
- **Protocol Handlers**: Implementation of MCP protocol handlers for resources, prompts, tools, and initialization
- **Factory**: Functions to create and configure an MCP server with custom backends and handlers
## Features
- Complete implementation of the MCP protocol version 2024-11-05
- Handles all boilerplate protocol methods (resources/list, tools/list, prompts/list, etc.)
- JSON-RPC based communication layer with automatic request/response handling
- Support for client-server capability negotiation
- Pluggable backend system for different storage and processing needs
- Generic type conversion utilities for MCP tool content
- Comprehensive error handling
- Logging capabilities
- Minimal implementation requirements for server developers
## Usage
To create a new MCP server using the core module:
```v
import freeflowuniverse.herolib.ai.mcp
import freeflowuniverse.herolib.schemas.jsonrpc
// Create a backend (memory-based or custom implementation)
backend := mcp.MemoryBackend{
tools: {
'my_tool': my_tool_definition
}
tool_handlers: {
'my_tool': my_tool_handler
}
}
// Create and configure the server
mut server := mcp.new_server(backend, mcp.ServerParams{
config: mcp.ServerConfiguration{
server_info: mcp.ServerInfo{
name: 'my_mcp_server'
version: '1.0.0'
}
}
})!
// Start the server
server.start()!
```

View File

@@ -0,0 +1,3 @@
If logic is implemented in mcp module, than structure with folders logic and mcp, where logic residers in /logic and mcp related code (like tool and prompt handlers and server code) in /mcp

View File

@@ -0,0 +1,32 @@
module mcp
import x.json2
interface Backend {
// Resource methods
resource_exists(uri string) !bool
resource_get(uri string) !Resource
resource_list() ![]Resource
resource_subscribed(uri string) !bool
resource_contents_get(uri string) ![]ResourceContent
resource_templates_list() ![]ResourceTemplate
// Prompt methods
prompt_exists(name string) !bool
prompt_get(name string) !Prompt
prompt_call(name string, arguments []string) ![]PromptMessage
prompt_list() ![]Prompt
prompt_messages_get(name string, arguments map[string]string) ![]PromptMessage
// Tool methods
tool_exists(name string) !bool
tool_get(name string) !Tool
tool_list() ![]Tool
tool_call(name string, arguments map[string]json2.Any) !ToolCallResult
// Sampling methods
sampling_create_message(params map[string]json2.Any) !SamplingCreateMessageResult
mut:
resource_subscribe(uri string) !
resource_unsubscribe(uri string) !
}

View File

@@ -0,0 +1,183 @@
module mcpcore
import x.json2
pub struct MemoryBackend {
pub mut:
// Resource related fields
resources map[string]Resource
subscriptions []string // list of subscribed resource uri's
resource_contents map[string][]ResourceContent
resource_templates map[string]ResourceTemplate
// Prompt related fields
prompts map[string]Prompt
prompt_messages map[string][]PromptMessage
prompt_handlers map[string]PromptHandler
// Tool related fields
tools map[string]Tool
tool_handlers map[string]ToolHandler
// Sampling related fields
sampling_handler SamplingHandler
}
pub type ToolHandler = fn (arguments map[string]json2.Any) !ToolCallResult
pub type PromptHandler = fn (arguments []string) ![]PromptMessage
pub type SamplingHandler = fn (params map[string]json2.Any) !SamplingCreateMessageResult
fn (b &MemoryBackend) resource_exists(uri string) !bool {
return uri in b.resources
}
fn (b &MemoryBackend) resource_get(uri string) !Resource {
return b.resources[uri] or { return error('resource not found') }
}
fn (b &MemoryBackend) resource_list() ![]Resource {
return b.resources.values()
}
fn (mut b MemoryBackend) resource_subscribe(uri string) ! {
if uri !in b.subscriptions {
b.subscriptions << uri
}
}
fn (b &MemoryBackend) resource_subscribed(uri string) !bool {
return uri in b.subscriptions
}
fn (mut b MemoryBackend) resource_unsubscribe(uri string) ! {
b.subscriptions = b.subscriptions.filter(it != uri)
}
fn (b &MemoryBackend) resource_contents_get(uri string) ![]ResourceContent {
return b.resource_contents[uri] or { return error('resource contents not found') }
}
fn (b &MemoryBackend) resource_templates_list() ![]ResourceTemplate {
return b.resource_templates.values()
}
// Prompt related methods
fn (b &MemoryBackend) prompt_exists(name string) !bool {
return name in b.prompts
}
fn (b &MemoryBackend) prompt_get(name string) !Prompt {
return b.prompts[name] or { return error('prompt not found') }
}
fn (b &MemoryBackend) prompt_list() ![]Prompt {
return b.prompts.values()
}
fn (b &MemoryBackend) prompt_messages_get(name string, arguments map[string]string) ![]PromptMessage {
// Get the base messages for this prompt
base_messages := b.prompt_messages[name] or { return error('prompt messages not found') }
// Apply arguments to the messages
mut messages := []PromptMessage{}
for msg in base_messages {
mut content := msg.content
// If the content is text, replace argument placeholders
if content.typ == 'text' {
mut text := content.text
// Replace each argument in the text
for arg_name, arg_value in arguments {
text = text.replace('{{${arg_name}}}', arg_value)
}
content = PromptContent{
typ: content.typ
text: text
data: content.data
mimetype: content.mimetype
resource: content.resource
}
}
messages << PromptMessage{
role: msg.role
content: content
}
}
return messages
}
fn (b &MemoryBackend) prompt_call(name string, arguments []string) ![]PromptMessage {
// Get the tool handler
handler := b.prompt_handlers[name] or { return error('tool handler not found') }
// Call the handler with the provided arguments
return handler(arguments) or { panic(err) }
}
// Tool related methods
fn (b &MemoryBackend) tool_exists(name string) !bool {
return name in b.tools
}
fn (b &MemoryBackend) tool_get(name string) !Tool {
return b.tools[name] or { return error('tool not found') }
}
fn (b &MemoryBackend) tool_list() ![]Tool {
return b.tools.values()
}
fn (b &MemoryBackend) tool_call(name string, arguments map[string]json2.Any) !ToolCallResult {
// Get the tool handler
handler := b.tool_handlers[name] or { return error('tool handler not found') }
// Call the handler with the provided arguments
return handler(arguments) or {
// If the handler throws an error, return it as a tool error
return ToolCallResult{
is_error: true
content: [
ToolContent{
typ: 'text'
text: 'Error: ${err.msg()}'
},
]
}
}
}
// Sampling related methods
fn (b &MemoryBackend) sampling_create_message(params map[string]json2.Any) !SamplingCreateMessageResult {
// Check if a sampling handler is registered
if isnil(b.sampling_handler) {
// Return a default implementation that just echoes back a message
// indicating that no sampling handler is registered
return SamplingCreateMessageResult{
model: 'default'
stop_reason: 'endTurn'
role: 'assistant'
content: MessageContent{
typ: 'text'
text: 'Sampling is not configured on this server. Please register a sampling handler.'
}
}
}
// Call the sampling handler with the provided parameters
return b.sampling_handler(params)!
}
// register_sampling_handler registers a handler for sampling requests
pub fn (mut b MemoryBackend) register_sampling_handler(handler SamplingHandler) {
b.sampling_handler = handler
}

68
lib/ai/mcpcore/cmd/compile.vsh Executable file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env -S v -n -cg -w -parallel-cc -enable-globals run
import os
import flag
mut fp := flag.new_flag_parser(os.args)
fp.application('compile.vsh')
fp.version('v0.1.0')
fp.description('Compile MCP binary in debug or production mode')
fp.skip_executable()
prod_mode := fp.bool('prod', `p`, false, 'Build production version (optimized)')
help_requested := fp.bool('help', `h`, false, 'Show help message')
if help_requested {
println(fp.usage())
exit(0)
}
additional_args := fp.finalize() or {
eprintln(err)
println(fp.usage())
exit(1)
}
if additional_args.len > 0 {
eprintln('Unexpected arguments: ${additional_args.join(' ')}')
println(fp.usage())
exit(1)
}
// Change to the mcp directory
mcp_dir := os.dir(os.real_path(os.executable()))
os.chdir(mcp_dir) or { panic('Failed to change directory to ${mcp_dir}: ${err}') }
// Set MCPPATH based on OS
mut mcppath := '/usr/local/bin/mcp'
if os.user_os() == 'macos' {
mcppath = os.join_path(os.home_dir(), 'hero/bin/mcp')
}
// Set compilation command based on OS and mode
compile_cmd := if prod_mode {
'v -enable-globals -w -n -prod mcp.v'
} else {
'v -w -cg -gc none -cc tcc -d use_openssl -enable-globals mcp.v'
}
println('Building MCP in ${if prod_mode { 'production' } else { 'debug' }} mode...')
if os.system(compile_cmd) != 0 {
panic('Failed to compile mcp.v with command: ${compile_cmd}')
}
// Make executable
os.chmod('mcp', 0o755) or { panic('Failed to make mcp binary executable: ${err}') }
// Ensure destination directory exists
os.mkdir_all(os.dir(mcppath)) or { panic('Failed to create directory ${os.dir(mcppath)}: ${err}') }
// Copy to destination paths
os.cp('mcp', mcppath) or { panic('Failed to copy mcp binary to ${mcppath}: ${err}') }
os.cp('mcp', '/tmp/mcp') or { panic('Failed to copy mcp binary to /tmp/mcp: ${err}') }
// Clean up
os.rm('mcp') or { panic('Failed to remove temporary mcp binary: ${err}') }
println('**MCP COMPILE OK**')

93
lib/ai/mcpcore/cmd/mcp.v Normal file
View File

@@ -0,0 +1,93 @@
module main
import os
import cli { Command, Flag }
import freeflowuniverse.herolib.osal.core as osal
// import freeflowuniverse.herolib.ai.mcp.vcode
// import freeflowuniverse.herolib.ai.mcp.mcpgen
// import freeflowuniverse.herolib.ai.mcp.baobab
import freeflowuniverse.herolib.ai.mcp.rhai.mcp as rhai_mcp
import freeflowuniverse.herolib.ai.mcp.rust
fn main() {
do() or { panic(err) }
}
pub fn do() ! {
mut cmd_mcp := Command{
name: 'mcp'
usage: '
## Manage your MCPs
example:
mcp
'
description: 'create, edit, show mdbooks'
required_args: 0
}
// cmd_run_add_flags(mut cmd_publisher)
cmd_mcp.add_flag(Flag{
flag: .bool
required: false
name: 'debug'
abbrev: 'd'
description: 'show debug output'
})
cmd_mcp.add_flag(Flag{
flag: .bool
required: false
name: 'verbose'
abbrev: 'v'
description: 'show verbose output'
})
mut cmd_inspector := Command{
sort_flags: true
name: 'inspector'
execute: cmd_inspector_execute
description: 'will list existing mdbooks'
}
cmd_inspector.add_flag(Flag{
flag: .string
required: false
name: 'name'
abbrev: 'n'
description: 'name of the MCP'
})
cmd_inspector.add_flag(Flag{
flag: .bool
required: false
name: 'open'
abbrev: 'o'
description: 'open inspector'
})
cmd_mcp.add_command(rhai_mcp.command)
cmd_mcp.add_command(rust.command)
// cmd_mcp.add_command(baobab.command)
// cmd_mcp.add_command(vcode.command)
cmd_mcp.add_command(cmd_inspector)
// cmd_mcp.add_command(vcode.command)
cmd_mcp.setup()
cmd_mcp.parse(os.args)
}
fn cmd_inspector_execute(cmd Command) ! {
open := cmd.flags.get_bool('open') or { false }
if open {
osal.exec(cmd: 'open http://localhost:5173')!
}
name := cmd.flags.get_string('name') or { '' }
if name.len > 0 {
println('starting inspector for MCP ${name}')
osal.exec(cmd: 'npx @modelcontextprotocol/inspector mcp ${name} start')!
} else {
osal.exec(cmd: 'npx @modelcontextprotocol/inspector')!
}
}

49
lib/ai/mcpcore/factory.v Normal file
View File

@@ -0,0 +1,49 @@
module mcp
import time
import os
import log
import x.json2
import freeflowuniverse.herolib.schemas.jsonrpc
@[params]
pub struct ServerParams {
pub:
handlers map[string]jsonrpc.ProcedureHandler
config ServerConfiguration
}
// new_server creates a new MCP server
pub fn new_server(backend Backend, params ServerParams) !&Server {
mut server := &Server{
ServerConfiguration: params.config
backend: backend
}
// Create a handler with the core MCP procedures registered
handler := jsonrpc.new_handler(jsonrpc.Handler{
procedures: {
//...params.handlers,
// Core handlers
'initialize': server.initialize_handler
'notifications/initialized': initialized_notification_handler
// Resource handlers
'resources/list': server.resources_list_handler
'resources/read': server.resources_read_handler
'resources/templates/list': server.resources_templates_list_handler
'resources/subscribe': server.resources_subscribe_handler
// Prompt handlers
'prompts/list': server.prompts_list_handler
'prompts/get': server.prompts_get_handler
'completion/complete': server.prompts_get_handler
// Tool handlers
'tools/list': server.tools_list_handler
'tools/call': server.tools_call_handler
// Sampling handlers
'sampling/createMessage': server.sampling_create_message_handler
}
})!
server.handler = *handler
return server
}

52
lib/ai/mcpcore/generics.v Normal file
View File

@@ -0,0 +1,52 @@
module mcp
pub fn result_to_mcp_tool_contents[T](result T) []ToolContent {
return [result_to_mcp_tool_content[T](result)]
}
pub fn result_to_mcp_tool_content[T](result T) ToolContent {
$if T is string {
return ToolContent{
typ: 'text'
text: result.str()
}
} $else $if T is int {
return ToolContent{
typ: 'number'
number: result.int()
}
} $else $if T is bool {
return ToolContent{
typ: 'boolean'
boolean: result.bool()
}
} $else $if result is $array {
mut items := []ToolContent{}
for item in result {
items << result_to_mcp_tool_content(item)
}
return ToolContent{
typ: 'array'
items: items
}
} $else $if T is $struct {
mut properties := map[string]ToolContent{}
$for field in T.fields {
properties[field.name] = result_to_mcp_tool_content(result.$(field.name))
}
return ToolContent{
typ: 'object'
properties: properties
}
} $else {
panic('Unsupported type: ${typeof(result)}')
}
}
pub fn array_to_mcp_tool_contents[U](array []U) []ToolContent {
mut contents := []ToolContent{}
for item in array {
contents << result_to_mcp_tool_content(item)
}
return contents
}

View File

@@ -0,0 +1,27 @@
module mcpcore
import time
import os
import log
import x.json2
import freeflowuniverse.herolib.schemas.jsonrpc
// initialize_handler handles the initialize request according to the MCP specification
fn (mut s Server) initialize_handler(data string) !string {
// Decode the request with ClientConfiguration parameters
request := jsonrpc.decode_request_generic[ClientConfiguration](data)!
s.client_config = request.params
// Create a success response with the result
response := jsonrpc.new_response_generic[ServerConfiguration](request.id, s.ServerConfiguration)
return response.encode()
}
// initialized_notification_handler handles the initialized notification
// This notification is sent by the client after successful initialization
fn initialized_notification_handler(data string) !string {
// This is a notification, so no response is expected
// Just log that we received the notification
log.info('Received initialized notification')
return ''
}

View File

@@ -0,0 +1,103 @@
module mcp
import freeflowuniverse.herolib.schemas.jsonrpc
import json
// This file contains tests for the MCP initialize handler implementation.
// It tests the handler's ability to process initialize requests according to the MCP specification.
// test_initialize_handler tests the initialize handler with a sample initialize request
fn test_initialize_handler() {
mut server := Server{}
// Sample initialize request from the MCP specification
initialize_request := '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"sampling":{},"roots":{"listChanged":true}},"clientInfo":{"name":"mcp-inspector","version":"0.0.1"}}}'
// Call the initialize handler directly
response := server.initialize_handler(initialize_request) or {
assert false, 'Initialize handler failed: ${err}'
return
}
// Decode the response to verify its structure
decoded_response := jsonrpc.decode_response(response) or {
assert false, 'Failed to decode response: ${err}'
return
}
// Verify that the response is not an error
assert !decoded_response.is_error(), 'Response should not be an error'
// Parse the result to verify its contents
result_json := decoded_response.result() or {
assert false, 'Failed to get result: ${err}'
return
}
// Decode the result into an ServerConfiguration struct
result := json.decode(ServerConfiguration, result_json) or {
assert false, 'Failed to decode result: ${err}'
return
}
// Verify the protocol version matches what was requested
assert result.protocol_version == '2024-11-05', 'Protocol version should match the request'
// Verify server capabilities
assert result.capabilities.prompts.list_changed == true, 'Prompts capability should have list_changed set to true'
assert result.capabilities.resources.subscribe == true, 'Resources capability should have subscribe set to true'
assert result.capabilities.resources.list_changed == true, 'Resources capability should have list_changed set to true'
assert result.capabilities.tools.list_changed == true, 'Tools capability should have list_changed set to true'
// Verify server info
assert result.server_info.name == 'HeroLibMCPServer', 'Server name should be HeroLibMCPServer'
assert result.server_info.version == '1.0.0', 'Server version should be 1.0.0'
}
// test_initialize_handler_with_handler tests the initialize handler through the JSONRPC handler
fn test_initialize_handler_with_handler() {
mut server := Server{}
// Create a handler with just the initialize procedure
handler := jsonrpc.new_handler(jsonrpc.Handler{
procedures: {
'initialize': server.initialize_handler
}
}) or {
assert false, 'Failed to create handler: ${err}'
return
}
// Sample initialize request from the MCP specification
initialize_request := '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"sampling":{},"roots":{"listChanged":true}},"clientInfo":{"name":"mcp-inspector","version":"0.0.1"}}}'
// Process the request through the handler
response := handler.handle(initialize_request) or {
assert false, 'Handler failed to process request: ${err}'
return
}
// Decode the response to verify its structure
decoded_response := jsonrpc.decode_response(response) or {
assert false, 'Failed to decode response: ${err}'
return
}
// Verify that the response is not an error
assert !decoded_response.is_error(), 'Response should not be an error'
// Parse the result to verify its contents
result_json := decoded_response.result() or {
assert false, 'Failed to get result: ${err}'
return
}
// Decode the result into an ServerConfiguration struct
result := json.decode(ServerConfiguration, result_json) or {
assert false, 'Failed to decode result: ${err}'
return
}
// Verify the protocol version matches what was requested
assert result.protocol_version == '2024-11-05', 'Protocol version should match the request'
}

View File

@@ -0,0 +1,135 @@
module mcp
import time
import os
import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
// Prompt related structs
pub struct Prompt {
pub:
name string
description string
arguments []PromptArgument
}
pub struct PromptArgument {
pub:
name string
description string
required bool
}
pub struct PromptMessage {
pub:
role string
content PromptContent
}
pub struct PromptContent {
pub:
typ string @[json: 'type']
text string
data string
mimetype string @[json: 'mimeType']
resource ResourceContent
}
// Prompt List Handler
pub struct PromptListParams {
pub:
cursor string
}
pub struct PromptListResult {
pub:
prompts []Prompt
next_cursor string @[json: 'nextCursor']
}
// prompts_list_handler handles the prompts/list request
// This request is used to retrieve a list of available prompts
fn (mut s Server) prompts_list_handler(data string) !string {
// Decode the request with cursor parameter
request := jsonrpc.decode_request_generic[PromptListParams](data)!
cursor := request.params.cursor
// TODO: Implement pagination logic using the cursor
// For now, return all prompts
// Create a success response with the result
response := jsonrpc.new_response_generic[PromptListResult](request.id, PromptListResult{
prompts: s.backend.prompt_list()!
next_cursor: '' // Empty if no more pages
})
return response.encode()
}
// Prompt Get Handler
pub struct PromptGetParams {
pub:
name string
arguments map[string]string
}
pub struct PromptGetResult {
pub:
description string
messages []PromptMessage
}
// prompts_get_handler handles the prompts/get request
// This request is used to retrieve a specific prompt with arguments
fn (mut s Server) prompts_get_handler(data string) !string {
// Decode the request with name and arguments parameters
request_map := json2.raw_decode(data)!.as_map()
params_map := request_map['params'].as_map()
if !s.backend.prompt_exists(params_map['name'].str())! {
return jsonrpc.new_error_response(request_map['id'].int(), prompt_not_found(params_map['name'].str())).encode()
}
// Get the prompt by name
prompt := s.backend.prompt_get(params_map['name'].str())!
// Validate required arguments
for arg in prompt.arguments {
if arg.required && params_map['arguments'].as_map()[arg.name].str() == '' {
return jsonrpc.new_error_response(request_map['id'].int(), missing_required_argument(arg.name)).encode()
}
}
messages := s.backend.prompt_call(params_map['name'].str(), params_map['arguments'].as_map().values().map(it.str()))!
// // Get the prompt messages with arguments applied
// messages := s.backend.prompt_messages_get(request.params.name, request.params.arguments)!
// Create a success response with the result
response := jsonrpc.new_response_generic[PromptGetResult](request_map['id'].int(),
PromptGetResult{
description: prompt.description
messages: messages
})
return response.encode()
}
// Prompt Notification Handlers
// send_prompts_list_changed_notification sends a notification when the list of prompts changes
pub fn (mut s Server) send_prompts_list_changed_notification() ! {
// Check if the client supports this notification
if !s.client_config.capabilities.roots.list_changed {
return
}
// Create a notification
notification := jsonrpc.new_blank_notification('notifications/prompts/list_changed')
s.send(json.encode(notification))
// Send the notification to all connected clients
log.info('Sending prompts list changed notification: ${json.encode(notification)}')
}

View File

@@ -0,0 +1,186 @@
module mcp
import time
import os
import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
pub struct Resource {
pub:
uri string
name string
description string
mimetype string @[json: 'mimeType']
}
// Resource List Handler
pub struct ResourceListParams {
pub:
cursor string
}
pub struct ResourceListResult {
pub:
resources []Resource
next_cursor string @[json: 'nextCursor']
}
// resources_list_handler handles the resources/list request
// This request is used to retrieve a list of available resources
fn (mut s Server) resources_list_handler(data string) !string {
// Decode the request with cursor parameter
request := jsonrpc.decode_request_generic[ResourceListParams](data)!
cursor := request.params.cursor
// TODO: Implement pagination logic using the cursor
// For now, return all resources
// Create a success response with the result
response := jsonrpc.new_response_generic[ResourceListResult](request.id, ResourceListResult{
resources: s.backend.resource_list()!
next_cursor: '' // Empty if no more pages
})
return response.encode()
}
// Resource Read Handler
pub struct ResourceReadParams {
pub:
uri string
}
pub struct ResourceReadResult {
pub:
contents []ResourceContent
}
pub struct ResourceContent {
pub:
uri string
mimetype string @[json: 'mimeType']
text string
blob string // Base64-encoded binary data
}
// resources_read_handler handles the resources/read request
// This request is used to retrieve the contents of a resource
fn (mut s Server) resources_read_handler(data string) !string {
// Decode the request with uri parameter
request := jsonrpc.decode_request_generic[ResourceReadParams](data)!
if !s.backend.resource_exists(request.params.uri)! {
return jsonrpc.new_error_response(request.id, resource_not_found(request.params.uri)).encode()
}
// Get the resource contents by URI
resource_contents := s.backend.resource_contents_get(request.params.uri)!
// Create a success response with the result
response := jsonrpc.new_response_generic[ResourceReadResult](request.id, ResourceReadResult{
contents: resource_contents
})
return response.encode()
}
// Resource Templates Handler
pub struct ResourceTemplatesListResult {
pub:
resource_templates []ResourceTemplate @[json: 'resourceTemplates']
}
pub struct ResourceTemplate {
pub:
uri_template string @[json: 'uriTemplate']
name string
description string
mimetype string @[json: 'mimeType']
}
// resources_templates_list_handler handles the resources/templates/list request
// This request is used to retrieve a list of available resource templates
fn (mut s Server) resources_templates_list_handler(data string) !string {
// Decode the request
request := jsonrpc.decode_request(data)!
// Create a success response with the result
response := jsonrpc.new_response_generic[ResourceTemplatesListResult](request.id,
ResourceTemplatesListResult{
resource_templates: s.backend.resource_templates_list()!
})
return response.encode()
}
// Resource Subscription Handler
pub struct ResourceSubscribeParams {
pub:
uri string
}
pub struct ResourceSubscribeResult {
pub:
subscribed bool
}
// resources_subscribe_handler handles the resources/subscribe request
// This request is used to subscribe to changes for a specific resource
fn (mut s Server) resources_subscribe_handler(data string) !string {
request := jsonrpc.decode_request_generic[ResourceSubscribeParams](data)!
if !s.backend.resource_exists(request.params.uri)! {
return jsonrpc.new_error_response(request.id, resource_not_found(request.params.uri)).encode()
}
s.backend.resource_subscribe(request.params.uri)!
response := jsonrpc.new_response_generic[ResourceSubscribeResult](request.id, ResourceSubscribeResult{
subscribed: true
})
return response.encode()
}
// Resource Notification Handlers
// send_resources_list_changed_notification sends a notification when the list of resources changes
pub fn (mut s Server) send_resources_list_changed_notification() ! {
// Check if the client supports this notification
if !s.client_config.capabilities.roots.list_changed {
return
}
// Create a notification
notification := jsonrpc.new_blank_notification('notifications/resources/list_changed')
s.send(json.encode(notification))
// Send the notification to all connected clients
// In a real implementation, this would use a WebSocket or other transport
log.info('Sending resources list changed notification: ${json.encode(notification)}')
}
pub struct ResourceUpdatedParams {
pub:
uri string
}
// send_resource_updated_notification sends a notification when a subscribed resource is updated
pub fn (mut s Server) send_resource_updated_notification(uri string) ! {
// Check if the client is subscribed to this resource
if !s.backend.resource_subscribed(uri)! {
return
}
// Create a notification
notification := jsonrpc.new_notification[ResourceUpdatedParams]('notifications/resources/updated',
ResourceUpdatedParams{
uri: uri
})
s.send(json.encode(notification))
// Send the notification to all connected clients
// In a real implementation, this would use a WebSocket or other transport
log.info('Sending resource updated notification: ${json.encode(notification)}')
}

View File

@@ -0,0 +1,145 @@
module mcp
import time
import os
import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
// Sampling related structs
pub struct MessageContent {
pub:
typ string @[json: 'type']
text string
data string
mimetype string @[json: 'mimeType']
}
pub struct Message {
pub:
role string
content MessageContent
}
pub struct ModelHint {
pub:
name string
}
pub struct ModelPreferences {
pub:
hints []ModelHint
cost_priority f32 @[json: 'costPriority']
speed_priority f32 @[json: 'speedPriority']
intelligence_priority f32 @[json: 'intelligencePriority']
}
pub struct SamplingCreateMessageParams {
pub:
messages []Message
model_preferences ModelPreferences @[json: 'modelPreferences']
system_prompt string @[json: 'systemPrompt']
include_context string @[json: 'includeContext']
temperature f32
max_tokens int @[json: 'maxTokens']
stop_sequences []string @[json: 'stopSequences']
metadata map[string]json2.Any
}
pub struct SamplingCreateMessageResult {
pub:
model string
stop_reason string @[json: 'stopReason']
role string
content MessageContent
}
// sampling_create_message_handler handles the sampling/createMessage request
// This request is used to request LLM completions through the client
fn (mut s Server) sampling_create_message_handler(data string) !string {
// Decode the request
request_map := json2.raw_decode(data)!.as_map()
id := request_map['id'].int()
params_map := request_map['params'].as_map()
// Validate required parameters
if 'messages' !in params_map {
return jsonrpc.new_error_response(id, missing_required_argument('messages')).encode()
}
if 'maxTokens' !in params_map {
return jsonrpc.new_error_response(id, missing_required_argument('maxTokens')).encode()
}
// Call the backend to handle the sampling request
result := s.backend.sampling_create_message(params_map) or {
return jsonrpc.new_error_response(id, sampling_error(err.msg())).encode()
}
// Create a success response with the result
response := jsonrpc.new_response(id, json.encode(result))
return response.encode()
}
// Helper function to convert JSON messages to our Message struct format
fn parse_messages(messages_json json2.Any) ![]Message {
messages_arr := messages_json.arr()
mut result := []Message{cap: messages_arr.len}
for msg_json in messages_arr {
msg_map := msg_json.as_map()
if 'role' !in msg_map {
return error('Missing role in message')
}
if 'content' !in msg_map {
return error('Missing content in message')
}
role := msg_map['role'].str()
content_map := msg_map['content'].as_map()
if 'type' !in content_map {
return error('Missing type in message content')
}
typ := content_map['type'].str()
mut text := ''
mut data := ''
mut mimetype := ''
if typ == 'text' {
if 'text' !in content_map {
return error('Missing text in text content')
}
text = content_map['text'].str()
} else if typ == 'image' {
if 'data' !in content_map {
return error('Missing data in image content')
}
data = content_map['data'].str()
if 'mimeType' !in content_map {
return error('Missing mimeType in image content')
}
mimetype = content_map['mimeType'].str()
} else {
return error('Unsupported content type: ${typ}')
}
result << Message{
role: role
content: MessageContent{
typ: typ
text: text
data: data
mimetype: mimetype
}
}
}
return result
}

View File

@@ -0,0 +1,151 @@
module mcp
import time
import os
import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.schemas.jsonschema
// Tool related structs
pub struct Tool {
pub:
name string
description string
input_schema jsonschema.Schema @[json: 'inputSchema']
}
pub struct ToolProperty {
pub:
typ string @[json: 'type']
items ToolItems
enum []string
}
pub struct ToolItems {
pub:
typ string @[json: 'type']
enum []string
properties map[string]ToolProperty
}
pub struct ToolContent {
pub:
typ string @[json: 'type']
text string
number int
boolean bool
properties map[string]ToolContent
items []ToolContent
}
// Tool List Handler
pub struct ToolListParams {
pub:
cursor string
}
pub struct ToolListResult {
pub:
tools []Tool
next_cursor string @[json: 'nextCursor']
}
// tools_list_handler handles the tools/list request
// This request is used to retrieve a list of available tools
fn (mut s Server) tools_list_handler(data string) !string {
// Decode the request with cursor parameter
request := jsonrpc.decode_request_generic[ToolListParams](data)!
cursor := request.params.cursor
// TODO: Implement pagination logic using the cursor
// For now, return all tools
encoded := json.encode(ToolListResult{
tools: s.backend.tool_list()!
next_cursor: '' // Empty if no more pages
})
// Create a success response with the result
response := jsonrpc.new_response(request.id, json.encode(ToolListResult{
tools: s.backend.tool_list()!
next_cursor: '' // Empty if no more pages
}))
return response.encode()
}
// Tool Call Handler
pub struct ToolCallParams {
pub:
name string
arguments map[string]json2.Any
meta map[string]json2.Any @[json: '_meta']
}
pub struct ToolCallResult {
pub:
is_error bool @[json: 'isError']
content []ToolContent
}
// tools_call_handler handles the tools/call request
// This request is used to call a specific tool with arguments
fn (mut s Server) tools_call_handler(data string) !string {
// Decode the request with name and arguments parameters
request_map := json2.raw_decode(data)!.as_map()
params_map := request_map['params'].as_map()
tool_name := params_map['name'].str()
if !s.backend.tool_exists(tool_name)! {
return jsonrpc.new_error_response(request_map['id'].int(), tool_not_found(tool_name)).encode()
}
arguments := params_map['arguments'].as_map()
// Get the tool by name
tool := s.backend.tool_get(tool_name)!
// Validate arguments against the input schema
// TODO: Implement proper JSON Schema validation
for req in tool.input_schema.required {
if req !in arguments {
return jsonrpc.new_error_response(request_map['id'].int(), missing_required_argument(req)).encode()
}
}
log.error('Calling tool: ${tool_name} with arguments: ${arguments}')
// Call the tool with the provided arguments
result := s.backend.tool_call(tool_name, arguments)!
log.error('Received result from tool: ${tool_name} with result: ${result}')
// Create a success response with the result
response := jsonrpc.new_response_generic[ToolCallResult](request_map['id'].int(),
result)
return response.encode()
}
// Tool Notification Handlers
// send_tools_list_changed_notification sends a notification when the list of tools changes
pub fn (mut s Server) send_tools_list_changed_notification() ! {
// Check if the client supports this notification
if !s.client_config.capabilities.roots.list_changed {
return
}
// Create a notification
notification := jsonrpc.new_blank_notification('notifications/tools/list_changed')
s.send(json.encode(notification))
// Send the notification to all connected clients
log.info('Sending tools list changed notification: ${json.encode(notification)}')
}
pub fn error_tool_call_result(err IError) ToolCallResult {
return ToolCallResult{
is_error: true
content: [ToolContent{
typ: 'text'
text: err.msg()
}]
}
}

View File

@@ -0,0 +1,93 @@
module mcp
import time
import os
import log
import x.json2
import freeflowuniverse.herolib.schemas.jsonrpc
const protocol_version = '2024-11-05'
// MCP server implementation using stdio transport
// Based on https://modelcontextprotocol.io/docs/concepts/transports
// ClientConfiguration represents the parameters for the initialize request
pub struct ClientConfiguration {
pub:
protocol_version string @[json: 'protocolVersion']
capabilities ClientCapabilities
client_info ClientInfo @[json: 'clientInfo']
}
// ClientCapabilities represents the client capabilities
pub struct ClientCapabilities {
pub:
roots RootsCapability // Ability to provide filesystem roots
sampling SamplingCapability // Support for LLM sampling requests
experimental ExperimentalCapability // Describes support for non-standard experimental features
}
// RootsCapability represents the roots capability
pub struct RootsCapability {
pub:
list_changed bool @[json: 'listChanged']
}
// SamplingCapability represents the sampling capability
pub struct SamplingCapability {}
// ExperimentalCapability represents the experimental capability
pub struct ExperimentalCapability {}
// ClientInfo represents the client information
pub struct ClientInfo {
pub:
name string
version string
}
// ServerConfiguration represents the server configuration
pub struct ServerConfiguration {
pub:
protocol_version string = '2024-11-05' @[json: 'protocolVersion']
capabilities ServerCapabilities
server_info ServerInfo @[json: 'serverInfo']
}
// ServerCapabilities represents the server capabilities
pub struct ServerCapabilities {
pub:
logging LoggingCapability
prompts PromptsCapability
resources ResourcesCapability
tools ToolsCapability
}
// LoggingCapability represents the logging capability
pub struct LoggingCapability {
}
// PromptsCapability represents the prompts capability
pub struct PromptsCapability {
pub:
list_changed bool = true @[json: 'listChanged']
}
// ResourcesCapability represents the resources capability
pub struct ResourcesCapability {
pub:
subscribe bool = true @[json: 'subscribe']
list_changed bool = true @[json: 'listChanged']
}
// ToolsCapability represents the tools capability
pub struct ToolsCapability {
pub:
list_changed bool = true @[json: 'listChanged']
}
// ServerInfo represents the server information
pub struct ServerInfo {
pub:
name string = 'HeroLibMCPServer'
version string = '1.0.0'
}

View File

@@ -0,0 +1,91 @@
module mcp
import freeflowuniverse.herolib.schemas.jsonrpc
import json
// This file contains tests for the MCP initialize handler implementation.
// It tests the handler's ability to process initialize requests according to the MCP specification.
// test_json_serialization_deserialization tests the JSON serialization and deserialization of initialize request and response
fn test_json_serialization_deserialization() {
// Create a sample initialize params object
params := ClientConfiguration{
protocol_version: '2024-11-05'
capabilities: ClientCapabilities{
roots: RootsCapability{
list_changed: true
}
// sampling: SamplingCapability{}
}
client_info: ClientInfo{
name: 'mcp-inspector'
// version: '0.0.1'
}
}
// Serialize the params to JSON
params_json := json.encode(params)
// Verify the JSON structure has the correct camelCase keys
assert params_json.contains('"protocolVersion":"2024-11-05"'), 'JSON should have protocolVersion in camelCase'
assert params_json.contains('"clientInfo":{'), 'JSON should have clientInfo in camelCase'
assert params_json.contains('"listChanged":true'), 'JSON should have listChanged in camelCase'
// Deserialize the JSON back to a struct
deserialized_params := json.decode(ClientConfiguration, params_json) or {
assert false, 'Failed to deserialize params: ${err}'
return
}
// Verify the deserialized object matches the original
assert deserialized_params.protocol_version == params.protocol_version, 'Deserialized protocol_version should match original'
assert deserialized_params.client_info.name == params.client_info.name, 'Deserialized client_info.name should match original'
assert deserialized_params.client_info.version == params.client_info.version, 'Deserialized client_info.version should match original'
assert deserialized_params.capabilities.roots.list_changed == params.capabilities.roots.list_changed, 'Deserialized capabilities.roots.list_changed should match original'
// Now test the response serialization/deserialization
response := ServerConfiguration{
protocol_version: '2024-11-05'
capabilities: ServerCapabilities{
logging: LoggingCapability{}
prompts: PromptsCapability{
list_changed: true
}
resources: ResourcesCapability{
subscribe: true
list_changed: true
}
tools: ToolsCapability{
list_changed: true
}
}
server_info: ServerInfo{
name: 'HeroLibMCPServer'
version: '1.0.0'
}
}
// Serialize the response to JSON
response_json := json.encode(response)
// Verify the JSON structure has the correct camelCase keys
assert response_json.contains('"protocolVersion":"2024-11-05"'), 'JSON should have protocolVersion in camelCase'
assert response_json.contains('"serverInfo":{'), 'JSON should have serverInfo in camelCase'
assert response_json.contains('"listChanged":true'), 'JSON should have listChanged in camelCase'
assert response_json.contains('"subscribe":true'), 'JSON should have subscribe field'
// Deserialize the JSON back to a struct
deserialized_response := json.decode(ServerConfiguration, response_json) or {
assert false, 'Failed to deserialize response: ${err}'
return
}
// Verify the deserialized object matches the original
assert deserialized_response.protocol_version == response.protocol_version, 'Deserialized protocol_version should match original'
assert deserialized_response.server_info.name == response.server_info.name, 'Deserialized server_info.name should match original'
assert deserialized_response.server_info.version == response.server_info.version, 'Deserialized server_info.version should match original'
assert deserialized_response.capabilities.prompts.list_changed == response.capabilities.prompts.list_changed, 'Deserialized capabilities.prompts.list_changed should match original'
assert deserialized_response.capabilities.resources.subscribe == response.capabilities.resources.subscribe, 'Deserialized capabilities.resources.subscribe should match original'
assert deserialized_response.capabilities.resources.list_changed == response.capabilities.resources.list_changed, 'Deserialized capabilities.resources.list_changed should match original'
assert deserialized_response.capabilities.tools.list_changed == response.capabilities.tools.list_changed, 'Deserialized capabilities.tools.list_changed should match original'
}

View File

@@ -0,0 +1,42 @@
module mcpcore
import freeflowuniverse.herolib.schemas.jsonrpc
// resource_not_found indicates that the requested resource doesn't exist.
// This error is returned when the resource specified in the request is not found.
// Error code: -32002
pub fn resource_not_found(uri string) jsonrpc.RPCError {
return jsonrpc.RPCError{
code: -32002
message: 'Resource not found'
data: 'The requested resource ${uri} was not found.'
}
}
fn prompt_not_found(name string) jsonrpc.RPCError {
return jsonrpc.RPCError{
code: -32602 // Invalid params
message: 'Prompt not found: ${name}'
}
}
fn missing_required_argument(arg_name string) jsonrpc.RPCError {
return jsonrpc.RPCError{
code: -32602 // Invalid params
message: 'Missing required argument: ${arg_name}'
}
}
fn tool_not_found(name string) jsonrpc.RPCError {
return jsonrpc.RPCError{
code: -32602 // Invalid params
message: 'Tool not found: ${name}'
}
}
fn sampling_error(message string) jsonrpc.RPCError {
return jsonrpc.RPCError{
code: -32603 // Internal error
message: 'Sampling error: ${message}'
}
}

56
lib/ai/mcpcore/server.v Normal file
View File

@@ -0,0 +1,56 @@
module mcp
import time
import os
import log
import x.json2
import freeflowuniverse.herolib.schemas.jsonrpc
// Server is the main MCP server struct
@[heap]
pub struct Server {
ServerConfiguration
pub mut:
client_config ClientConfiguration
handler jsonrpc.Handler
backend Backend
}
// start starts the MCP server
pub fn (mut s Server) start() ! {
log.info('Starting MCP server')
for {
// Read a message from stdin
message := os.get_line()
if message == '' {
time.sleep(10000) // prevent cpu spinning
continue
}
// Handle the message using the JSON-RPC handler
response := s.handler.handle(message) or {
log.error('message: ${message}')
log.error('Error handling message: ${err}')
// Try to extract the request ID
id := jsonrpc.decode_request_id(message) or { 0 }
// Create an internal error response
error_response := jsonrpc.new_error(id, jsonrpc.internal_error).encode()
print(error_response)
continue
}
// Send the response only if it's not empty (notifications return empty responses)
if response.len > 0 {
s.send(response)
}
}
}
// send sends a response to the client
pub fn (mut s Server) send(response string) {
// Send the response
println(response)
flush_stdout()
}