Merge branch 'development_actions007_timur' into development

* development_actions007_timur:
  progress in mail code gen
  vcode and baobab mcps enhancements
  generator mcp integration progress
  fix encoding of non-generic jsonrpc response
  add baobab methods for easier mcp integration
  implement more code functionalities
  reorganize and sort mcps
  manual updates

# Conflicts:
#	lib/mcp/README.md
This commit is contained in:
2025-04-01 09:53:51 +02:00
67 changed files with 4428 additions and 1519 deletions

View File

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

View File

@@ -1,391 +0,0 @@
module developer
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.mcp
import os
// create_mcp_tool_code receives the name of a V language function string, and the path to the module in which it exists.
// returns an MCP Tool code in v for attaching the function to the mcp server
pub fn (d &Developer) create_mcp_tool_code(function_name string, module_path string) !string {
println('DEBUG: Looking for function ${function_name} in module path: ${module_path}')
if !os.exists(module_path) {
println('DEBUG: Module path does not exist: ${module_path}')
return error('Module path does not exist: ${module_path}')
}
function_ := get_function_from_module(module_path, function_name)!
println('Function string found:\n${function_}')
// Try to parse the function
function := code.parse_function(function_) or {
println('Error parsing function: ${err}')
return error('Failed to parse function: ${err}')
}
mut types := map[string]string{}
for param in function.params {
// Check if the type is an Object (struct)
if param.typ is code.Object {
types[param.typ.symbol()] = get_type_from_module(module_path, param.typ.symbol())!
}
}
// Get the result type if it's a struct
mut result_ := ""
if function.result.typ is code.Result {
result_type := (function.result.typ as code.Result).typ
if result_type is code.Object {
result_ = get_type_from_module(module_path, result_type.symbol())!
}
} else if function.result.typ is code.Object {
result_ = get_type_from_module(module_path, function.result.typ.symbol())!
}
tool_name := function.name
tool := d.create_mcp_tool(function_, types)!
handler := d.create_mcp_tool_handler(function_, types, result_)!
str := $tmpl('./templates/tool_code.v.template')
return str
}
// create_mcp_tool parses a V language function string and returns an MCP Tool struct
// function: The V function string including preceding comments
// types: A map of struct names to their definitions for complex parameter types
// result: The type of result of the create_mcp_tool function. Could be simply string, or struct {...}
pub fn (d &Developer) create_mcp_tool_handler(function_ string, types map[string]string, result_ string) !string {
function := code.parse_function(function_)!
decode_stmts := function.params.map(argument_decode_stmt(it)).join_lines()
result := code.parse_type(result_)
str := $tmpl('./templates/tool_handler.v.template')
return str
}
pub fn argument_decode_stmt(param code.Param) string {
return if param.typ is code.Integer {
'${param.name} := arguments["${param.name}"].int()'
} else if param.typ is code.Boolean {
'${param.name} := arguments["${param.name}"].bool()'
} else if param.typ is code.String {
'${param.name} := arguments["${param.name}"].str()'
} else if param.typ is code.Object {
'${param.name} := json.decode[${param.typ.symbol()}](arguments["${param.name}"].str())!'
} else if param.typ is code.Array {
'${param.name} := json.decode[${param.typ.symbol()}](arguments["${param.name}"].str())!'
} else if param.typ is code.Map {
'${param.name} := json.decode[${param.typ.symbol()}](arguments["${param.name}"].str())!'
} else {
panic('Unsupported type: ${param.typ}')
}
}
/*
in @generate_mcp.v , implement a create_mpc_tool_handler function that given a vlang function string and the types that map to their corresponding type definitions (for instance struct some_type: SomeType{...}), generates a vlang function such as the following:
ou
pub fn (d &Developer) create_mcp_tool_tool_handler(arguments map[string]Any) !mcp.Tool {
function := arguments['function'].str()
types := json.decode[map[string]string](arguments['types'].str())!
return d.create_mcp_tool(function, types)
}
*/
// create_mcp_tool parses a V language function string and returns an MCP Tool struct
// function: The V function string including preceding comments
// types: A map of struct names to their definitions for complex parameter types
pub fn (d Developer) create_mcp_tool(function string, types map[string]string) !mcp.Tool {
// Extract description from preceding comments
mut description := ''
lines := function.split('\n')
// Find function signature line
mut fn_line_idx := -1
for i, line in lines {
if line.trim_space().starts_with('fn ') || line.trim_space().starts_with('pub fn ') {
fn_line_idx = i
break
}
}
if fn_line_idx == -1 {
return error('Invalid function: no function signature found')
}
// Extract comments before the function
for i := 0; i < fn_line_idx; i++ {
line := lines[i].trim_space()
if line.starts_with('//') {
// Remove the comment marker and any leading space
comment := line[2..].trim_space()
if description != '' {
description += '\n'
}
description += comment
}
}
// Parse function signature
fn_signature := lines[fn_line_idx].trim_space()
// Extract function name
mut fn_name := ''
// Check if this is a method with a receiver
if fn_signature.contains('fn (') {
// This is a method with a receiver
// Format: [pub] fn (receiver Type) name(...)
// Find the closing parenthesis of the receiver
mut receiver_end := fn_signature.index(')') or { return error('Invalid method signature: missing closing parenthesis for receiver') }
// Extract the text after the receiver
mut after_receiver := fn_signature[receiver_end + 1..].trim_space()
// Extract the function name (everything before the opening parenthesis)
mut params_start := after_receiver.index('(') or { return error('Invalid method signature: missing parameters') }
fn_name = after_receiver[0..params_start].trim_space()
} else if fn_signature.starts_with('pub fn ') {
// Regular public function
mut prefix_len := 'pub fn '.len
mut params_start := fn_signature.index('(') or { return error('Invalid function signature: missing parameters') }
fn_name = fn_signature[prefix_len..params_start].trim_space()
} else if fn_signature.starts_with('fn ') {
// Regular function
mut prefix_len := 'fn '.len
mut params_start := fn_signature.index('(') or { return error('Invalid function signature: missing parameters') }
fn_name = fn_signature[prefix_len..params_start].trim_space()
} else {
return error('Invalid function signature: must start with "fn" or "pub fn"')
}
if fn_name == '' {
return error('Could not extract function name')
}
// Extract parameters
mut params_str := ''
// Check if this is a method with a receiver
if fn_signature.contains('fn (') {
// This is a method with a receiver
// Find the closing parenthesis of the receiver
mut receiver_end := fn_signature.index(')') or { return error('Invalid method signature: missing closing parenthesis for receiver') }
// Find the opening parenthesis of the parameters
mut params_start := -1
for i := receiver_end + 1; i < fn_signature.len; i++ {
if fn_signature[i] == `(` {
params_start = i
break
}
}
if params_start == -1 {
return error('Invalid method signature: missing parameter list')
}
// Find the closing parenthesis of the parameters
mut params_end := fn_signature.last_index(')') or { return error('Invalid method signature: missing closing parenthesis for parameters') }
// Extract the parameters
params_str = fn_signature[params_start + 1..params_end].trim_space()
} else {
// Regular function
mut params_start := fn_signature.index('(') or { return error('Invalid function signature: missing parameters') }
mut params_end := fn_signature.last_index(')') or { return error('Invalid function signature: missing closing parenthesis') }
// Extract the parameters
params_str = fn_signature[params_start + 1..params_end].trim_space()
}
// Create input schema for parameters
mut properties := map[string]mcp.ToolProperty{}
mut required := []string{}
if params_str != '' {
param_list := params_str.split(',')
for param in param_list {
trimmed_param := param.trim_space()
if trimmed_param == '' {
continue
}
// Split parameter into name and type
param_parts := trimmed_param.split_any(' \t')
if param_parts.len < 2 {
continue
}
param_name := param_parts[0]
param_type := param_parts[1]
// Add to required parameters
required << param_name
// Create property for this parameter
mut property := mcp.ToolProperty{}
// Check if this is a complex type defined in the types map
if param_type in types {
// Parse the struct definition to create a nested schema
struct_def := types[param_type]
struct_schema := d.create_mcp_tool_input_schema(struct_def)!
property = mcp.ToolProperty{
typ: struct_schema.typ
}
} else {
// Handle primitive types
schema := d.create_mcp_tool_input_schema(param_type)!
property = mcp.ToolProperty{
typ: schema.typ
}
}
properties[param_name] = property
}
}
// Create the input schema
input_schema := mcp.ToolInputSchema{
typ: 'object',
properties: properties,
required: required
}
// Create and return the Tool
return mcp.Tool{
name: fn_name,
description: description,
input_schema: input_schema
}
}
// create_mcp_tool_input_schema creates a ToolInputSchema for a given input type
// input: The input type string
// returns: A ToolInputSchema for the given input type
// errors: Returns an error if the input type is not supported
pub fn (d Developer) create_mcp_tool_input_schema(input string) !mcp.ToolInputSchema {
// if input is a primitive type, return a mcp ToolInputSchema with that type
if input == 'string' {
return mcp.ToolInputSchema{
typ: 'string'
}
} else if input == 'int' {
return mcp.ToolInputSchema{
typ: 'integer'
}
} else if input == 'float' {
return mcp.ToolInputSchema{
typ: 'number'
}
} else if input == 'bool' {
return mcp.ToolInputSchema{
typ: 'boolean'
}
}
// if input is a struct, return a mcp ToolInputSchema with typ 'object' and properties for each field in the struct
if input.starts_with('pub struct ') {
struct_name := input[11..].split(' ')[0]
fields := parse_struct_fields(input)
mut properties := map[string]mcp.ToolProperty{}
for field_name, field_type in fields {
property := mcp.ToolProperty{
typ: d.create_mcp_tool_input_schema(field_type)!.typ
}
properties[field_name] = property
}
return mcp.ToolInputSchema{
typ: 'object',
properties: properties
}
}
// if input is an array, return a mcp ToolInputSchema with typ 'array' and items of the item type
if input.starts_with('[]') {
item_type := input[2..]
// For array types, we create a schema with type 'array'
// The actual item type is determined by the primitive type
mut item_type_str := 'string' // default
if item_type == 'int' {
item_type_str = 'integer'
} else if item_type == 'float' {
item_type_str = 'number'
} else if item_type == 'bool' {
item_type_str = 'boolean'
}
// Create a property for the array items
mut property := mcp.ToolProperty{
typ: 'array'
}
// Add the property to the schema
mut properties := map[string]mcp.ToolProperty{}
properties['items'] = property
return mcp.ToolInputSchema{
typ: 'array',
properties: properties
}
}
// Default to string type for unknown types
return mcp.ToolInputSchema{
typ: 'string'
}
}
// parse_struct_fields parses a V language struct definition string and returns a map of field names to their types
fn parse_struct_fields(struct_def string) map[string]string {
mut fields := map[string]string{}
// Find the opening and closing braces of the struct definition
start_idx := struct_def.index('{') or { return fields }
end_idx := struct_def.last_index('}') or { return fields }
// Extract the content between the braces
struct_content := struct_def[start_idx + 1..end_idx].trim_space()
// Split the content by newlines to get individual field definitions
field_lines := struct_content.split('
')
for line in field_lines {
trimmed_line := line.trim_space()
// Skip empty lines and comments
if trimmed_line == '' || trimmed_line.starts_with('//') {
continue
}
// Handle pub: or mut: prefixes
mut field_def := trimmed_line
if field_def.starts_with('pub:') || field_def.starts_with('mut:') {
field_def = field_def.all_after(':').trim_space()
}
// Split by whitespace to separate field name and type
parts := field_def.split_any(' ')
if parts.len < 2 {
continue
}
field_name := parts[0]
field_type := parts[1..].join(' ')
// Handle attributes like @[json: 'name']
if field_name.contains('@[') {
continue
}
fields[field_name] = field_type
}
return fields
}

View File

@@ -1,205 +0,0 @@
module developer
import freeflowuniverse.herolib.mcp
import json
import os
// fn test_parse_struct_fields() {
// // Test case 1: Simple struct with primitive types
// simple_struct := 'pub struct User {
// name string
// age int
// active bool
// }'
// fields := parse_struct_fields(simple_struct)
// assert fields.len == 3
// assert fields['name'] == 'string'
// assert fields['age'] == 'int'
// assert fields['active'] == 'bool'
// // Test case 2: Struct with pub: and mut: sections
// complex_struct := 'pub struct Config {
// pub:
// host string
// port int
// mut:
// connected bool
// retries int
// }'
// fields2 := parse_struct_fields(complex_struct)
// assert fields2.len == 4
// assert fields2['host'] == 'string'
// assert fields2['port'] == 'int'
// assert fields2['connected'] == 'bool'
// assert fields2['retries'] == 'int'
// // Test case 3: Struct with attributes and comments
// struct_with_attrs := 'pub struct ApiResponse {
// // User ID
// id int
// // User full name
// name string @[json: "full_name"]
// // Whether account is active
// active bool
// }'
// fields3 := parse_struct_fields(struct_with_attrs)
// assert fields3.len == 3 // All fields are included
// assert fields3['id'] == 'int'
// assert fields3['active'] == 'bool'
// // Test case 4: Empty struct
// empty_struct := 'pub struct Empty {}'
// fields4 := parse_struct_fields(empty_struct)
// assert fields4.len == 0
// println('test_parse_struct_fields passed')
// }
// fn test_create_mcp_tool_input_schema() {
// d := Developer{}
// // Test case 1: Primitive types
// string_schema := d.create_mcp_tool_input_schema('string') or { panic(err) }
// assert string_schema.typ == 'string'
// int_schema := d.create_mcp_tool_input_schema('int') or { panic(err) }
// assert int_schema.typ == 'integer'
// float_schema := d.create_mcp_tool_input_schema('float') or { panic(err) }
// assert float_schema.typ == 'number'
// bool_schema := d.create_mcp_tool_input_schema('bool') or { panic(err) }
// assert bool_schema.typ == 'boolean'
// // Test case 2: Array type
// array_schema := d.create_mcp_tool_input_schema('[]string') or { panic(err) }
// assert array_schema.typ == 'array'
// // In our implementation, arrays don't have items directly in the schema
// // Test case 3: Struct type
// struct_def := 'pub struct Person {
// name string
// age int
// }'
// struct_schema := d.create_mcp_tool_input_schema(struct_def) or { panic(err) }
// assert struct_schema.typ == 'object'
// assert struct_schema.properties.len == 2
// assert struct_schema.properties['name'].typ == 'string'
// assert struct_schema.properties['age'].typ == 'integer'
// println('test_create_mcp_tool_input_schema passed')
// }
// fn test_create_mcp_tool() {
// d := Developer{}
// // Test case 1: Simple function with primitive types
// simple_fn := '// Get user by ID
// // Returns user information
// pub fn get_user(id int, include_details bool) {
// // Implementation
// }'
// tool1 := d.create_mcp_tool(simple_fn, {}) or { panic(err) }
// assert tool1.name == 'get_user'
// expected_desc1 := 'Get user by ID\nReturns user information'
// assert tool1.description == expected_desc1
// assert tool1.input_schema.typ == 'object'
// assert tool1.input_schema.properties.len == 2
// assert tool1.input_schema.properties['id'].typ == 'integer'
// assert tool1.input_schema.properties['include_details'].typ == 'boolean'
// assert tool1.input_schema.required.len == 2
// assert 'id' in tool1.input_schema.required
// assert 'include_details' in tool1.input_schema.required
// // Test case 2: Method with receiver
// method_fn := '// Update user profile
// pub fn (u User) update_profile(name string, age int) bool {
// // Implementation
// return true
// }'
// tool2 := d.create_mcp_tool(method_fn, {}) or { panic(err) }
// assert tool2.name == 'update_profile'
// assert tool2.description == 'Update user profile'
// assert tool2.input_schema.properties.len == 2
// assert tool2.input_schema.properties['name'].typ == 'string'
// assert tool2.input_schema.properties['age'].typ == 'integer'
// // Test case 3: Function with complex types
// complex_fn := '// Create new configuration
// // Sets up system configuration
// fn create_config(name string, settings Config) !Config {
// // Implementation
// }'
// config_struct := 'pub struct Config {
// server_url string
// max_retries int
// timeout float
// }'
// tool3 := d.create_mcp_tool(complex_fn, {
// 'Config': config_struct
// }) or { panic(err) }
// assert tool3.name == 'create_config'
// expected_desc3 := 'Create new configuration\nSets up system configuration'
// assert tool3.description == expected_desc3
// assert tool3.input_schema.properties.len == 2
// assert tool3.input_schema.properties['name'].typ == 'string'
// assert tool3.input_schema.properties['settings'].typ == 'object'
// // Test case 4: Function with no parameters
// no_params_fn := '// Initialize system
// pub fn initialize() {
// // Implementation
// }'
// tool4 := d.create_mcp_tool(no_params_fn, {}) or { panic(err) }
// assert tool4.name == 'initialize'
// assert tool4.description == 'Initialize system'
// assert tool4.input_schema.properties.len == 0
// assert tool4.input_schema.required.len == 0
// println('test_create_mcp_tool passed')
// }
// fn test_create_mcp_tool_code() {
// d := Developer{}
// // Test with the complex function that has struct parameters and return type
// module_path := "${os.dir(@FILE)}/testdata/mock_module"
// function_name := 'test_function'
// code := d.create_mcp_tool_code(function_name, module_path) or {
// panic('Failed to create MCP tool code: ${err}')
// }
// // Print the code instead of panic for debugging
// println('Generated code:')
// println('----------------------------------------')
// println(code)
// println('----------------------------------------')
// // Verify the generated code contains the expected elements
// assert code.contains('test_function_tool')
// assert code.contains('TestConfig')
// assert code.contains('TestResult')
// // Test with a simple function that has primitive types
// simple_function_name := 'simple_function'
// simple_code := d.create_mcp_tool_code(simple_function_name, module_path) or {
// panic('Failed to create MCP tool code for simple function: ${err}')
// }
// // Verify the simple function code
// assert simple_code.contains('simple_function_tool')
// assert simple_code.contains('name string')
// assert simple_code.contains('count int')
// // println('test_create_mcp_tool_code passed')
// }

View File

@@ -1,108 +0,0 @@
module developer
import freeflowuniverse.herolib.mcp
import x.json2 as json { Any }
// import json
const create_mcp_tool_code_tool = mcp.Tool{
name: 'create_mcp_tool_code'
description: 'create_mcp_tool_code receives the name of a V language function string, and the path to the module in which it exists.
returns an MCP Tool code in v for attaching the function to the mcp server'
input_schema: mcp.ToolInputSchema{
typ: 'object'
properties: {
'function_name': mcp.ToolProperty{
typ: 'string'
items: mcp.ToolItems{
typ: ''
enum: []
}
enum: []
}
'module_path': mcp.ToolProperty{
typ: 'string'
items: mcp.ToolItems{
typ: ''
enum: []
}
enum: []
}
}
required: ['function_name', 'module_path']
}
}
pub fn (d &Developer) create_mcp_tool_code_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
function_name := arguments['function_name'].str()
module_path := arguments['module_path'].str()
result := d.create_mcp_tool_code(function_name, module_path) or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: result_to_mcp_tool_contents[string](result)
}
}
// Tool definition for the create_mcp_tool function
const create_mcp_tool_tool = mcp.Tool{
name: 'create_mcp_tool'
description: 'Parses a V language function string and returns an MCP Tool struct. This tool analyzes function signatures, extracts parameters, and generates the appropriate MCP Tool representation.'
input_schema: mcp.ToolInputSchema{
typ: 'object'
properties: {
'function': mcp.ToolProperty{
typ: 'string'
}
'types': mcp.ToolProperty{
typ: 'object'
}
}
required: ['function']
}
}
pub fn (d &Developer) create_mcp_tool_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
function := arguments['function'].str()
types := json.decode[map[string]string](arguments['types'].str())!
result := d.create_mcp_tool(function, types) or { return mcp.error_tool_call_result(err) }
return mcp.ToolCallResult{
is_error: false
content: result_to_mcp_tool_contents[string](result.str())
}
}
// Tool definition for the create_mcp_tool_handler function
const create_mcp_tool_handler_tool = mcp.Tool{
name: 'create_mcp_tool_handler'
description: 'Generates a tool handler for the create_mcp_tool function. This tool handler accepts function string and types map and returns an MCP ToolCallResult.'
input_schema: mcp.ToolInputSchema{
typ: 'object'
properties: {
'function': mcp.ToolProperty{
typ: 'string'
}
'types': mcp.ToolProperty{
typ: 'object'
}
'result': mcp.ToolProperty{
typ: 'string'
}
}
required: ['function', 'result']
}
}
// Tool handler for the create_mcp_tool_handler function
pub fn (d &Developer) create_mcp_tool_handler_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
function := arguments['function'].str()
types := json.decode[map[string]string](arguments['types'].str())!
result_ := arguments['result'].str()
result := d.create_mcp_tool_handler(function, types, result_) or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: result_to_mcp_tool_contents[string](result)
}
}

View File

@@ -1,31 +0,0 @@
module developer
import freeflowuniverse.herolib.mcp.logger
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.schemas.jsonrpc
// pub fn new_mcp_server(d &Developer) !&mcp.Server {
// logger.info('Creating new Developer MCP server')
// // Initialize the server with the empty handlers map
// mut server := mcp.new_server(mcp.MemoryBackend{
// tools: {
// 'create_mcp_tool': create_mcp_tool_tool
// 'create_mcp_tool_handler': create_mcp_tool_handler_tool
// 'create_mcp_tool_code': create_mcp_tool_code_tool
// }
// tool_handlers: {
// 'create_mcp_tool': d.create_mcp_tool_tool_handler
// 'create_mcp_tool_handler': d.create_mcp_tool_handler_tool_handler
// 'create_mcp_tool_code': d.create_mcp_tool_code_tool_handler
// }
// }, mcp.ServerParams{
// config: mcp.ServerConfiguration{
// server_info: mcp.ServerInfo{
// name: 'developer'
// version: '1.0.0'
// }
// }
// })!
// return server
// }

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.mcp.developer
import freeflowuniverse.herolib.mcp.logger
mut server := developer.new_mcp_server(&developer.Developer{})!
server.start() or {
logger.fatal('Error starting server: ${err}')
exit(1)
}

View File

@@ -1,11 +0,0 @@
pub fn (d &Developer) @{function.name}_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
@{decode_stmts}
result := d.@{function.name}(@{function.params.map(it.name).join(',')})
or {
return error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: result_to_mcp_tool_content[@{result.symbol()}](result)
}
}

View File

@@ -1,38 +0,0 @@
module mock_module
// TestConfig represents a configuration for testing
pub struct TestConfig {
pub:
name string
enabled bool
count int
value float64
}
// TestResult represents the result of a test operation
pub struct TestResult {
pub:
success bool
message string
code int
}
// test_function is a simple function for testing the MCP tool code generation
// It takes a config and returns a result
pub fn test_function(config TestConfig) !TestResult {
// This is just a mock implementation for testing purposes
if config.name == '' {
return error('Name cannot be empty')
}
return TestResult{
success: config.enabled
message: 'Test completed for ${config.name}'
code: if config.enabled { 0 } else { 1 }
}
}
// simple_function is a function with primitive types for testing
pub fn simple_function(name string, count int) string {
return '${name} count: ${count}'
}

View File

@@ -1,159 +0,0 @@
module main
import os
// Standalone test for the get_type_from_module function
// This file can be run directly with: v run vlang_test_standalone.v
// Implementation of get_type_from_module function
fn get_type_from_module(module_path string, type_name string) !string {
v_files := list_v_files(module_path) or {
return error('Failed to list V files in ${module_path}: ${err}')
}
for v_file in v_files {
content := os.read_file(v_file) or { return error('Failed to read file ${v_file}: ${err}') }
type_str := 'struct ${type_name} {'
i := content.index(type_str) or { -1 }
if i == -1 {
continue
}
start_i := i + type_str.len
closing_i := find_closing_brace(content, start_i) or {
return error('could not find where declaration for type ${type_name} ends')
}
return content.substr(start_i, closing_i + 1)
}
return error('type ${type_name} not found in module ${module_path}')
}
// Helper function to find the closing brace
fn find_closing_brace(content string, start_i int) ?int {
mut brace_count := 1
for i := start_i; i < content.len; i++ {
if content[i] == `{` {
brace_count++
} else if content[i] == `}` {
brace_count--
if brace_count == 0 {
return i
}
}
}
return none
}
// Helper function to list V files
fn list_v_files(dir string) ![]string {
files := os.ls(dir) or { return error('Error listing directory: ${err}') }
mut v_files := []string{}
for file in files {
if file.ends_with('.v') && !file.ends_with('_.v') {
filepath := os.join_path(dir, file)
v_files << filepath
}
}
return v_files
}
// Helper function to create test files with struct definitions
fn create_test_files() !(string, string, string) {
// Create a temporary directory for our test files
test_dir := os.temp_dir()
test_file_path := os.join_path(test_dir, 'test_type.v')
// Create a test file with a simple struct
test_content := 'module test_module
struct TestType {
name string
age int
active bool
}
// Another struct to make sure we get the right one
struct OtherType {
id string
}
'
os.write_file(test_file_path, test_content) or {
eprintln('Failed to create test file: ${err}')
return error('Failed to create test file: ${err}')
}
// Create a test file with a nested struct
nested_test_content := 'module test_module
struct NestedType {
config map[string]string {
required: true
}
data []struct {
key string
value string
}
}
'
nested_test_file := os.join_path(test_dir, 'nested_test.v')
os.write_file(nested_test_file, nested_test_content) or {
eprintln('Failed to create nested test file: ${err}')
return error('Failed to create nested test file: ${err}')
}
return test_dir, test_file_path, nested_test_file
}
// Test function for get_type_from_module
fn test_get_type_from_module() {
// Create test files
test_dir, test_file_path, nested_test_file := create_test_files() or {
eprintln('Failed to create test files: ${err}')
assert false
return
}
// Test case 1: Get a simple struct
type_content := get_type_from_module(test_dir, 'TestType') or {
eprintln('Failed to get type: ${err}')
assert false
return
}
// Verify the content matches what we expect
expected := '\n\tname string\n\tage int\n\tactive bool\n}'
assert type_content == expected, 'Expected: "${expected}", got: "${type_content}"'
// Test case 2: Try to get a non-existent type
non_existent := get_type_from_module(test_dir, 'NonExistentType') or {
// This should fail, so we expect an error
assert err.str().contains('not found in module'), 'Expected error message about type not found'
''
}
assert non_existent == '', 'Expected empty string for non-existent type'
// Test case 3: Test with nested braces in the struct
nested_type_content := get_type_from_module(test_dir, 'NestedType') or {
eprintln('Failed to get nested type: ${err}')
assert false
return
}
expected_nested := '\n\tconfig map[string]string {\n\t\trequired: true\n\t}\n\tdata []struct {\n\t\tkey string\n\t\tvalue string\n\t}\n}'
assert nested_type_content == expected_nested, 'Expected: "${expected_nested}", got: "${nested_type_content}"'
// Clean up test files
os.rm(test_file_path) or {}
os.rm(nested_test_file) or {}
println('All tests for get_type_from_module passed successfully!')
}
fn main() {
test_get_type_from_module()
}

View File

@@ -1,34 +0,0 @@
module developer
import freeflowuniverse.herolib.mcp
const get_function_from_file_tool = mcp.Tool{
name: 'get_function_from_file'
description: 'get_function_from_file parses a V file and extracts a specific function block including its comments
ARGS:
file_path string - path to the V file
function_name string - name of the function to extract
RETURNS: string - the function block including comments, or empty string if not found'
input_schema: mcp.ToolInputSchema{
typ: 'object'
properties: {
'file_path': mcp.ToolProperty{
typ: 'string'
items: mcp.ToolItems{
typ: ''
enum: []
}
enum: []
}
'function_name': mcp.ToolProperty{
typ: 'string'
items: mcp.ToolItems{
typ: ''
enum: []
}
enum: []
}
}
required: ['file_path', 'function_name']
}
}

View File

@@ -2,14 +2,31 @@ module generator
import freeflowuniverse.herolib.core.code { Array, CodeItem, Function, Import, Param, Result, Struct, VFile }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.openapi
import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.schemas.openrpc.codegen { content_descriptor_to_parameter, content_descriptor_to_struct }
import freeflowuniverse.herolib.schemas.jsonschema { Schema }
import freeflowuniverse.herolib.schemas.jsonschema.codegen as jsonschema_codegen
import freeflowuniverse.herolib.baobab.specification { ActorMethod, ActorSpecification }
import log
const crud_prefixes = ['new', 'get', 'set', 'delete', 'list']
pub struct Source {
openapi_path ?string
openrpc_path ?string
}
pub fn generate_methods_file_str(source Source) !string {
actor_spec := if path := source.openapi_path {
specification.from_openapi(openapi.new(path: path)!)!
} else if path := source.openrpc_path {
specification.from_openrpc(openrpc.new(path: path)!)!
}
else { panic('No openapi or openrpc path provided') }
return generate_methods_file(actor_spec)!.write_str()!
}
pub fn generate_methods_file(spec ActorSpecification) !VFile {
name_snake := texttools.snake_case(spec.name)
actor_name_pascal := texttools.pascal_case(spec.name)
@@ -81,14 +98,17 @@ pub fn generate_method_code(receiver Param, method ActorMethod) ![]CodeItem {
// check if method is a Base Object CRUD Method and
// if so generate the method's body
body := match method.category {
.base_object_new { base_object_new_body(receiver, method)! }
.base_object_get { base_object_get_body(receiver, method)! }
.base_object_set { base_object_set_body(receiver, method)! }
.base_object_delete { base_object_delete_body(receiver, method)! }
.base_object_list { base_object_list_body(receiver, method)! }
else { "panic('implement')" }
}
// TODO: smart generation of method body using AI
// body := match method.category {
// .base_object_new { base_object_new_body(receiver, method)! }
// .base_object_get { base_object_get_body(receiver, method)! }
// .base_object_set { base_object_set_body(receiver, method)! }
// .base_object_delete { base_object_delete_body(receiver, method)! }
// .base_object_list { base_object_list_body(receiver, method)! }
// else { "panic('implement')" }
// }
body := "panic('implement')"
fn_prototype := generate_method_prototype(receiver, method)!
method_code << Function{
@@ -136,7 +156,9 @@ fn base_object_delete_body(receiver Param, method ActorMethod) !string {
}
fn base_object_list_body(receiver Param, method ActorMethod) !string {
result := content_descriptor_to_parameter(method.result)!
base_object_type := (result.typ as Array).typ
return 'return ${receiver.name}.osis.list[${base_object_type.symbol()}]()!'
// result := content_descriptor_to_parameter(method.result)!
// log.error('result typ: ${result.typ}')
// base_object_type := (result.typ as Array).typ
// return 'return ${receiver.name}.osis.list[${base_object_type.symbol()}]()!'
return 'return'
}

View File

@@ -7,6 +7,17 @@ import freeflowuniverse.herolib.schemas.jsonschema
import freeflowuniverse.herolib.schemas.jsonschema.codegen as jsonschema_codegen
import freeflowuniverse.herolib.schemas.openrpc.codegen { content_descriptor_to_parameter }
import freeflowuniverse.herolib.baobab.specification { ActorMethod, ActorSpecification }
import freeflowuniverse.herolib.schemas.openapi
pub fn generate_methods_example_file_str(source Source) !string {
actor_spec := if path := source.openapi_path {
specification.from_openapi(openapi.new(path: path)!)!
} else if path := source.openrpc_path {
specification.from_openrpc(openrpc.new(path: path)!)!
}
else { panic('No openapi or openrpc path provided') }
return generate_methods_example_file(actor_spec)!.write_str()!
}
pub fn generate_methods_example_file(spec ActorSpecification) !VFile {
name_snake := texttools.snake_case(spec.name)

View File

@@ -4,6 +4,18 @@ import freeflowuniverse.herolib.core.code { CodeItem, Import, Param, VFile }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.openrpc.codegen
import freeflowuniverse.herolib.baobab.specification { ActorSpecification }
import freeflowuniverse.herolib.schemas.openapi
import freeflowuniverse.herolib.schemas.openrpc
pub fn generate_methods_interface_file_str(source Source) !string {
actor_spec := if path := source.openapi_path {
specification.from_openapi(openapi.new(path: path)!)!
} else if path := source.openrpc_path {
specification.from_openrpc(openrpc.new(path: path)!)!
}
else { panic('No openapi or openrpc path provided') }
return generate_methods_interface_file(actor_spec)!.write_str()!
}
pub fn generate_methods_interface_file(spec ActorSpecification) !VFile {
return VFile{

View File

@@ -4,6 +4,18 @@ import freeflowuniverse.herolib.core.code { CodeItem, Struct, VFile }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.jsonschema.codegen { schema_to_struct }
import freeflowuniverse.herolib.baobab.specification { ActorSpecification }
import freeflowuniverse.herolib.schemas.openapi
import freeflowuniverse.herolib.schemas.openrpc
pub fn generate_model_file_str(source Source) !string {
actor_spec := if path := source.openapi_path {
specification.from_openapi(openapi.new(path: path)!)!
} else if path := source.openrpc_path {
specification.from_openrpc(openrpc.new(path: path)!)!
}
else { panic('No openapi or openrpc path provided') }
return generate_model_file(actor_spec)!.write_str()!
}
pub fn generate_model_file(spec ActorSpecification) !VFile {
actor_name_snake := texttools.snake_case(spec.name)

View File

@@ -3,7 +3,7 @@ module specification
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.core.code { Struct }
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef }
import freeflowuniverse.herolib.schemas.openapi { MediaType, OpenAPI, Parameter }
import freeflowuniverse.herolib.schemas.openapi { MediaType, OpenAPI, Parameter, Operation, OperationInfo }
import freeflowuniverse.herolib.schemas.openrpc { ContentDescriptor, ErrorSpec, Example, ExamplePairing, ExampleRef }
// Helper function: Convert OpenAPI parameter to ContentDescriptor

View File

@@ -0,0 +1,58 @@
module mail
import freeflowuniverse.herolib.baobab.osis {OSIS}
module mail
import freeflowuniverse.herolib.baobab.osis { OSIS }
pub struct HeroLibCirclesMailAPI {
mut:
osis OSIS
}
pub fn new_herolibcirclesmailapi() !HeroLibCirclesMailAPI {
return HeroLibCirclesMailAPI{osis: osis.new()!}
}
// Returns a list of all emails in the system
pub fn (mut h HeroLibCirclesMailAPI) list_emails(mailbox string) ![]Email {
panic('implement')
}
// Creates a new email in the system
pub fn (mut h HeroLibCirclesMailAPI) create_email(data EmailCreate) !Email {
panic('implement')
}
// Returns a single email by ID
pub fn (mut h HeroLibCirclesMailAPI) get_email_by_id(id u32) !Email {
panic('implement')
}
// Updates an existing email
pub fn (mut h HeroLibCirclesMailAPI) update_email(id u32, data EmailUpdate) !Email {
panic('implement')
}
// Deletes an email
pub fn (mut h HeroLibCirclesMailAPI) delete_email(id u32) ! {
panic('implement')
}
// Search for emails by various criteria
pub fn (mut h HeroLibCirclesMailAPI) search_emails(subject string, from string, to string, content string, date_from i64, date_to i64, has_attachments bool) ![]Email {
panic('implement')
}
// Returns all emails in a specific mailbox
pub fn (mut h HeroLibCirclesMailAPI) get_emails_by_mailbox(mailbox string) ![]Email {
panic('implement')
}
pub struct UpdateEmailFlags {
flags []string
}
// Update the flags of an email by its UID
pub fn (mut h HeroLibCirclesMailAPI) update_email_flags(uid u32, data UpdateEmailFlags) !Email {
panic('implement')
}

View File

@@ -0,0 +1,55 @@
module mail
import freeflowuniverse.herolib.baobab.osis {OSIS}
import x.json2 {as json}
module mail
import freeflowuniverse.herolib.baobab.osis {OSIS}
import x.json2 as json
pub struct HeroLibCirclesMailAPIExample {
osis OSIS
}
pub fn new_hero_lib_circles_mail_a_p_i_example() !HeroLibCirclesMailAPIExample {
return HeroLibCirclesMailAPIExample{osis: osis.new()!}
}
// Returns a list of all emails in the system
pub fn (mut h HeroLibCirclesMailAPIExample) list_emails(mailbox string) ![]Email {
json_str := '[]'
return json.decode[[]Email](json_str)!
}
// Creates a new email in the system
pub fn (mut h HeroLibCirclesMailAPIExample) create_email(data EmailCreate) !Email {
json_str := '{}'
return json.decode[Email](json_str)!
}
// Returns a single email by ID
pub fn (mut h HeroLibCirclesMailAPIExample) get_email_by_id(id u32) !Email {
json_str := '{}'
return json.decode[Email](json_str)!
}
// Updates an existing email
pub fn (mut h HeroLibCirclesMailAPIExample) update_email(id u32, data EmailUpdate) !Email {
json_str := '{}'
return json.decode[Email](json_str)!
}
// Deletes an email
pub fn (mut h HeroLibCirclesMailAPIExample) delete_email(id u32) ! {
// Implementation would go here
}
// Search for emails by various criteria
pub fn (mut h HeroLibCirclesMailAPIExample) search_emails(subject string, from string, to string, content string, date_from i64, date_to i64, has_attachments bool) ![]Email {
json_str := '[]'
return json.decode[[]Email](json_str)!
}
// Returns all emails in a specific mailbox
pub fn (mut h HeroLibCirclesMailAPIExample) get_emails_by_mailbox(mailbox string) ![]Email {
json_str := '[]'
return json.decode[[]Email](json_str)!
}
// Update the flags of an email by its UID
pub fn (mut h HeroLibCirclesMailAPIExample) update_email_flags(uid u32, data UpdateEmailFlags) !Email {
json_str := '{}'
return json.decode[Email](json_str)!
}

View File

@@ -0,0 +1,295 @@
module mail
import freeflowuniverse.herolib.baobab.osis {OSIS}
import x.json2 {as json}
module mail
import freeflowuniverse.herolib.baobab.osis { OSIS }
import x.json2 as json
pub struct HeroLibCirclesMailAPIExample {
osis OSIS
}
pub fn new_hero_lib_circles_mail_api_example() !HeroLibCirclesMailAPIExample {
return HeroLibCirclesMailAPIExample{osis: osis.new()!}
}
// Returns a list of all emails in the system
pub fn (mut h HeroLibCirclesMailAPIExample) list_emails(mailbox string) ![]Email {
// Example data from the OpenAPI spec
example_email1 := Email{
id: 1
uid: 101
seq_num: 1
mailbox: 'INBOX'
message: 'Hello, this is a test email.'
attachments: []
flags: ['\\Seen']
internal_date: 1647356400
size: 256
envelope: Envelope{
date: 1647356400
subject: 'Test Email'
from: ['sender@example.com']
sender: ['sender@example.com']
reply_to: ['sender@example.com']
to: ['recipient@example.com']
cc: []
bcc: []
in_reply_to: ''
message_id: '<abc123@example.com>'
}
}
example_email2 := Email{
id: 2
uid: 102
seq_num: 2
mailbox: 'INBOX'
message: 'This is another test email with an attachment.'
attachments: [
Attachment{
filename: 'document.pdf'
content_type: 'application/pdf'
data: 'base64encodeddata'
}
]
flags: []
internal_date: 1647442800
size: 1024
envelope: Envelope{
date: 1647442800
subject: 'Email with Attachment'
from: ['sender2@example.com']
sender: ['sender2@example.com']
reply_to: ['sender2@example.com']
to: ['recipient@example.com']
cc: ['cc@example.com']
bcc: []
in_reply_to: ''
message_id: '<def456@example.com>'
}
}
// Filter by mailbox if provided
if mailbox != '' && mailbox != 'INBOX' {
return []Email{}
}
return [example_email1, example_email2]
}
// Creates a new email in the system
pub fn (mut h HeroLibCirclesMailAPIExample) create_email(data EmailCreate) !Email {
// Example created email from OpenAPI spec
return Email{
id: 3
uid: 103
seq_num: 3
mailbox: data.mailbox
message: data.message
attachments: data.attachments
flags: data.flags
internal_date: 1647529200
size: 128
envelope: Envelope{
date: 1647529200
subject: data.envelope.subject
from: data.envelope.from
sender: data.envelope.from
reply_to: data.envelope.from
to: data.envelope.to
cc: data.envelope.cc
bcc: data.envelope.bcc
in_reply_to: ''
message_id: '<ghi789@example.com>'
}
}
}
// Returns a single email by ID
pub fn (mut h HeroLibCirclesMailAPIExample) get_email_by_id(id u32) !Email {
// Example email from OpenAPI spec
if id == 1 {
return Email{
id: 1
uid: 101
seq_num: 1
mailbox: 'INBOX'
message: 'Hello, this is a test email.'
attachments: []
flags: ['\\Seen']
internal_date: 1647356400
size: 256
envelope: Envelope{
date: 1647356400
subject: 'Test Email'
from: ['sender@example.com']
sender: ['sender@example.com']
reply_to: ['sender@example.com']
to: ['recipient@example.com']
cc: []
bcc: []
in_reply_to: ''
message_id: '<abc123@example.com>'
}
}
}
return error('Email not found')
}
// Updates an existing email
pub fn (mut h HeroLibCirclesMailAPIExample) update_email(id u32, data EmailUpdate) !Email {
// Example updated email from OpenAPI spec
if id == 1 {
return Email{
id: 1
uid: 101
seq_num: 1
mailbox: data.mailbox
message: data.message
attachments: data.attachments
flags: data.flags
internal_date: 1647356400
size: 300
envelope: Envelope{
date: 1647356400
subject: data.envelope.subject
from: data.envelope.from
sender: data.envelope.from
reply_to: data.envelope.from
to: data.envelope.to
cc: data.envelope.cc
bcc: data.envelope.bcc
in_reply_to: ''
message_id: '<abc123@example.com>'
}
}
}
return error('Email not found')
}
// Deletes an email
pub fn (mut h HeroLibCirclesMailAPIExample) delete_email(id u32) ! {
if id < 1 {
return error('Email not found')
}
// In a real implementation, this would delete the email
}
// Search for emails by various criteria
pub fn (mut h HeroLibCirclesMailAPIExample) search_emails(subject string, from string, to string, content string, date_from i64, date_to i64, has_attachments bool) ![]Email {
// Example search results from OpenAPI spec
return [
Email{
id: 1
uid: 101
seq_num: 1
mailbox: 'INBOX'
message: 'Hello, this is a test email with search terms.'
attachments: []
flags: ['\\Seen']
internal_date: 1647356400
size: 256
envelope: Envelope{
date: 1647356400
subject: 'Test Email Search'
from: ['sender@example.com']
sender: ['sender@example.com']
reply_to: ['sender@example.com']
to: ['recipient@example.com']
cc: []
bcc: []
in_reply_to: ''
message_id: '<abc123@example.com>'
}
}
]
}
// Returns all emails in a specific mailbox
pub fn (mut h HeroLibCirclesMailAPIExample) get_emails_by_mailbox(mailbox string) ![]Email {
// Example mailbox emails from OpenAPI spec
if mailbox == 'INBOX' {
return [
Email{
id: 1
uid: 101
seq_num: 1
mailbox: 'INBOX'
message: 'Hello, this is a test email in INBOX.'
attachments: []
flags: ['\\Seen']
internal_date: 1647356400
size: 256
envelope: Envelope{
date: 1647356400
subject: 'Test Email INBOX'
from: ['sender@example.com']
sender: ['sender@example.com']
reply_to: ['sender@example.com']
to: ['recipient@example.com']
cc: []
bcc: []
in_reply_to: ''
message_id: '<abc123@example.com>'
}
},
Email{
id: 2
uid: 102
seq_num: 2
mailbox: 'INBOX'
message: 'This is another test email in INBOX.'
attachments: []
flags: []
internal_date: 1647442800
size: 200
envelope: Envelope{
date: 1647442800
subject: 'Another Test Email INBOX'
from: ['sender2@example.com']
sender: ['sender2@example.com']
reply_to: ['sender2@example.com']
to: ['recipient@example.com']
cc: []
bcc: []
in_reply_to: ''
message_id: '<def456@example.com>'
}
}
]
}
return error('Mailbox not found')
}
// Update the flags of an email by its UID
pub fn (mut h HeroLibCirclesMailAPIExample) update_email_flags(uid u32, data UpdateEmailFlags) !Email {
// Example updated flags from OpenAPI spec
if uid == 101 {
return Email{
id: 1
uid: 101
seq_num: 1
mailbox: 'INBOX'
message: 'Hello, this is a test email.'
attachments: []
flags: data.flags
internal_date: 1647356400
size: 256
envelope: Envelope{
date: 1647356400
subject: 'Test Email'
from: ['sender@example.com']
sender: ['sender@example.com']
reply_to: ['sender@example.com']
to: ['recipient@example.com']
cc: []
bcc: []
in_reply_to: ''
message_id: '<abc123@example.com>'
}
}
}
return error('Email not found')
}

View File

@@ -0,0 +1,18 @@
module mail
import freeflowuniverse.herolib.baobab.osis {OSIS}
module mail
import freeflowuniverse.herolib.baobab.osis { OSIS }
// Interface for Mail API
pub interface IHeroLibCirclesMailAPI {
mut:
list_emails(string) ![]Email
create_email(EmailCreate) !Email
get_email_by_id(u32) !Email
update_email(u32, EmailUpdate) !Email
delete_email(u32) !
search_emails(string, string, string, string, i64, i64, bool) ![]Email
get_emails_by_mailbox(string) ![]Email
update_email_flags(u32, UpdateEmailFlags) !Email
}

59
lib/circles/mail/model_.v Normal file
View File

@@ -0,0 +1,59 @@
module mail
module mail
pub struct Email {
id int // Database ID (assigned by DBHandler)
uid int // Unique identifier of the message (in the circle)
seq_num int // IMAP sequence number (in the mailbox)
mailbox string // The mailbox this email belongs to
message string // The email body content
attachments []Attachment // Any file attachments
flags []string // IMAP flags like \Seen, \Deleted, etc.
internal_date int // Unix timestamp when the email was received
size int // Size of the message in bytes
envelope Envelope
}
pub struct EmailCreate {
mailbox string // The mailbox this email belongs to
message string // The email body content
attachments []Attachment // Any file attachments
flags []string // IMAP flags like \Seen, \Deleted, etc.
envelope EnvelopeCreate
}
pub struct EmailUpdate {
mailbox string // The mailbox this email belongs to
message string // The email body content
attachments []Attachment // Any file attachments
flags []string // IMAP flags like \Seen, \Deleted, etc.
envelope EnvelopeCreate
}
pub struct Attachment {
filename string // Name of the attached file
content_type string // MIME type of the attachment
data string // Base64 encoded binary data
}
pub struct Envelope {
date int // Unix timestamp of the email date
subject string // Email subject
from []string // From addresses
sender []string // Sender addresses
reply_to []string // Reply-To addresses
to []string // To addresses
cc []string // CC addresses
bcc []string // BCC addresses
in_reply_to string // Message ID this email is replying to
message_id string // Unique message ID
}
pub struct EnvelopeCreate {
subject string // Email subject
from []string // From addresses
to []string // To addresses
cc []string // CC addresses
bcc []string // BCC addresses
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,841 @@
{
"openrpc": "1.2.6",
"info": {
"title": "HeroLib Circles Mail API",
"description": "API for Mail functionality of HeroLib Circles MCC module. This API provides endpoints for managing emails.",
"version": "1.0.0",
"contact": {
"name": "FreeFlow Universe",
"url": "https://freeflowuniverse.org"
}
},
"servers": [
{
"url": "https://api.example.com/v1",
"name": "Production server"
},
{
"url": "https://dev-api.example.com/v1",
"name": "Development server"
}
],
"methods": [
{
"name": "listEmails",
"summary": "List all emails",
"description": "Returns a list of all emails in the system",
"params": [
{
"name": "mailbox",
"description": "Filter emails by mailbox",
"required": false,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "emails",
"description": "A list of emails",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Email"
}
},
"examples": [
{
"name": "listEmails",
"value": [
{
"id": 1,
"uid": 101,
"seq_num": 1,
"mailbox": "INBOX",
"message": "Hello, this is a test email.",
"attachments": [],
"flags": ["\\Seen"],
"internal_date": 1647356400,
"size": 256,
"envelope": {
"date": 1647356400,
"subject": "Test Email",
"from": ["sender@example.com"],
"sender": ["sender@example.com"],
"reply_to": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": [],
"bcc": [],
"in_reply_to": "",
"message_id": "<abc123@example.com>"
}
},
{
"id": 2,
"uid": 102,
"seq_num": 2,
"mailbox": "INBOX",
"message": "This is another test email with an attachment.",
"attachments": [
{
"filename": "document.pdf",
"content_type": "application/pdf",
"data": "base64encodeddata"
}
],
"flags": [],
"internal_date": 1647442800,
"size": 1024,
"envelope": {
"date": 1647442800,
"subject": "Email with Attachment",
"from": ["sender2@example.com"],
"sender": ["sender2@example.com"],
"reply_to": ["sender2@example.com"],
"to": ["recipient@example.com"],
"cc": ["cc@example.com"],
"bcc": [],
"in_reply_to": "",
"message_id": "<def456@example.com>"
}
}
]
}
]
}
},
{
"name": "createEmail",
"summary": "Create a new email",
"description": "Creates a new email in the system",
"params": [
{
"name": "data",
"description": "Email data to create",
"required": true,
"schema": {
"$ref": "#/components/schemas/EmailCreate"
},
"examples": [
{
"name": "createEmail",
"value": {
"mailbox": "INBOX",
"message": "This is a new email message.",
"attachments": [],
"flags": [],
"envelope": {
"subject": "New Email",
"from": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": [],
"bcc": []
}
}
}
]
}
],
"result": {
"name": "email",
"description": "The created email",
"schema": {
"$ref": "#/components/schemas/Email"
},
"examples": [
{
"name": "createdEmail",
"value": {
"id": 3,
"uid": 103,
"seq_num": 3,
"mailbox": "INBOX",
"message": "This is a new email message.",
"attachments": [],
"flags": [],
"internal_date": 1647529200,
"size": 128,
"envelope": {
"date": 1647529200,
"subject": "New Email",
"from": ["sender@example.com"],
"sender": ["sender@example.com"],
"reply_to": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": [],
"bcc": [],
"in_reply_to": "",
"message_id": "<ghi789@example.com>"
}
}
}
]
}
},
{
"name": "getEmailById",
"summary": "Get email by ID",
"description": "Returns a single email by ID",
"params": [
{
"name": "id",
"description": "ID of the email to retrieve",
"required": true,
"schema": {
"type": "integer",
"format": "uint32"
}
}
],
"result": {
"name": "email",
"description": "The requested email",
"schema": {
"$ref": "#/components/schemas/Email"
},
"examples": [
{
"name": "emailById",
"value": {
"id": 1,
"uid": 101,
"seq_num": 1,
"mailbox": "INBOX",
"message": "Hello, this is a test email.",
"attachments": [],
"flags": ["\\Seen"],
"internal_date": 1647356400,
"size": 256,
"envelope": {
"date": 1647356400,
"subject": "Test Email",
"from": ["sender@example.com"],
"sender": ["sender@example.com"],
"reply_to": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": [],
"bcc": [],
"in_reply_to": "",
"message_id": "<abc123@example.com>"
}
}
}
]
}
},
{
"name": "updateEmail",
"summary": "Update email",
"description": "Updates an existing email",
"params": [
{
"name": "id",
"description": "ID of the email to update",
"required": true,
"schema": {
"type": "integer",
"format": "uint32"
}
},
{
"name": "data",
"description": "Updated email data",
"required": true,
"schema": {
"$ref": "#/components/schemas/EmailUpdate"
},
"examples": [
{
"name": "updateEmail",
"value": {
"mailbox": "INBOX",
"message": "This is an updated email message.",
"attachments": [],
"flags": ["\\Seen"],
"envelope": {
"subject": "Updated Email",
"from": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": ["cc@example.com"],
"bcc": []
}
}
}
]
}
],
"result": {
"name": "email",
"description": "The updated email",
"schema": {
"$ref": "#/components/schemas/Email"
},
"examples": [
{
"name": "updatedEmail",
"value": {
"id": 1,
"uid": 101,
"seq_num": 1,
"mailbox": "INBOX",
"message": "This is an updated email message.",
"attachments": [],
"flags": ["\\Seen"],
"internal_date": 1647356400,
"size": 300,
"envelope": {
"date": 1647356400,
"subject": "Updated Email",
"from": ["sender@example.com"],
"sender": ["sender@example.com"],
"reply_to": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": ["cc@example.com"],
"bcc": [],
"in_reply_to": "",
"message_id": "<abc123@example.com>"
}
}
}
]
}
},
{
"name": "deleteEmail",
"summary": "Delete email",
"description": "Deletes an email",
"params": [
{
"name": "id",
"description": "ID of the email to delete",
"required": true,
"schema": {
"type": "integer",
"format": "uint32"
}
}
],
"result": {
"name": "success",
"description": "Success status",
"schema": {
"type": "null"
}
}
},
{
"name": "searchEmails",
"summary": "Search emails",
"description": "Search for emails by various criteria",
"params": [
{
"name": "subject",
"description": "Search in email subject",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "from",
"description": "Search in from field",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "to",
"description": "Search in to field",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "content",
"description": "Search in email content",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "date_from",
"description": "Filter by date from (Unix timestamp)",
"required": false,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "date_to",
"description": "Filter by date to (Unix timestamp)",
"required": false,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "has_attachments",
"description": "Filter by presence of attachments",
"required": false,
"schema": {
"type": "boolean"
}
}
],
"result": {
"name": "emails",
"description": "Search results",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Email"
}
},
"examples": [
{
"name": "searchResults",
"value": [
{
"id": 1,
"uid": 101,
"seq_num": 1,
"mailbox": "INBOX",
"message": "Hello, this is a test email with search terms.",
"attachments": [],
"flags": ["\\Seen"],
"internal_date": 1647356400,
"size": 256,
"envelope": {
"date": 1647356400,
"subject": "Test Email Search",
"from": ["sender@example.com"],
"sender": ["sender@example.com"],
"reply_to": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": [],
"bcc": [],
"in_reply_to": "",
"message_id": "<abc123@example.com>"
}
}
]
}
]
}
},
{
"name": "getEmailsByMailbox",
"summary": "Get emails by mailbox",
"description": "Returns all emails in a specific mailbox",
"params": [
{
"name": "mailbox",
"description": "Name of the mailbox",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "emails",
"description": "Emails in the mailbox",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Email"
}
},
"examples": [
{
"name": "mailboxEmails",
"value": [
{
"id": 1,
"uid": 101,
"seq_num": 1,
"mailbox": "INBOX",
"message": "Hello, this is a test email.",
"attachments": [],
"flags": ["\\Seen"],
"internal_date": 1647356400,
"size": 256,
"envelope": {
"date": 1647356400,
"subject": "Test Email",
"from": ["sender@example.com"],
"sender": ["sender@example.com"],
"reply_to": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": [],
"bcc": [],
"in_reply_to": "",
"message_id": "<abc123@example.com>"
}
},
{
"id": 2,
"uid": 102,
"seq_num": 2,
"mailbox": "INBOX",
"message": "This is another test email with an attachment.",
"attachments": [
{
"filename": "document.pdf",
"content_type": "application/pdf",
"data": "base64encodeddata"
}
],
"flags": [],
"internal_date": 1647442800,
"size": 1024,
"envelope": {
"date": 1647442800,
"subject": "Email with Attachment",
"from": ["sender2@example.com"],
"sender": ["sender2@example.com"],
"reply_to": ["sender2@example.com"],
"to": ["recipient@example.com"],
"cc": ["cc@example.com"],
"bcc": [],
"in_reply_to": "",
"message_id": "<def456@example.com>"
}
}
]
}
]
}
},
{
"name": "updateEmailFlags",
"summary": "Update email flags",
"description": "Update the flags of an email by its UID",
"params": [
{
"name": "uid",
"description": "UID of the email to update flags",
"required": true,
"schema": {
"type": "integer",
"format": "uint32"
}
},
{
"name": "data",
"description": "Flag update data",
"required": true,
"schema": {
"$ref": "#/components/schemas/UpdateEmailFlags"
},
"examples": [
{
"name": "updateFlags",
"value": {
"flags": ["\\Seen", "\\Flagged"]
}
}
]
}
],
"result": {
"name": "email",
"description": "The email with updated flags",
"schema": {
"$ref": "#/components/schemas/Email"
},
"examples": [
{
"name": "updatedFlags",
"value": {
"id": 1,
"uid": 101,
"seq_num": 1,
"mailbox": "INBOX",
"message": "Hello, this is a test email.",
"attachments": [],
"flags": ["\\Seen", "\\Flagged"],
"internal_date": 1647356400,
"size": 256,
"envelope": {
"date": 1647356400,
"subject": "Test Email",
"from": ["sender@example.com"],
"sender": ["sender@example.com"],
"reply_to": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": [],
"bcc": [],
"in_reply_to": "",
"message_id": "<abc123@example.com>"
}
}
}
]
}
}
],
"components": {
"schemas": {
"Email": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "uint32",
"description": "Unique identifier for the email"
},
"uid": {
"type": "integer",
"format": "uint32",
"description": "IMAP UID of the email"
},
"seq_num": {
"type": "integer",
"format": "uint32",
"description": "Sequence number of the email"
},
"mailbox": {
"type": "string",
"description": "Mailbox the email belongs to"
},
"message": {
"type": "string",
"description": "Content of the email"
},
"attachments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Attachment"
},
"description": "List of attachments"
},
"flags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Email flags"
},
"internal_date": {
"type": "integer",
"format": "int64",
"description": "Internal date of the email (Unix timestamp)"
},
"size": {
"type": "integer",
"format": "uint32",
"description": "Size of the email in bytes"
},
"envelope": {
"$ref": "#/components/schemas/Envelope",
"description": "Email envelope information"
}
},
"required": ["id", "uid", "mailbox", "message", "envelope"]
},
"EmailCreate": {
"type": "object",
"properties": {
"mailbox": {
"type": "string",
"description": "Mailbox to create the email in"
},
"message": {
"type": "string",
"description": "Content of the email"
},
"attachments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Attachment"
},
"description": "List of attachments"
},
"flags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Email flags"
},
"envelope": {
"$ref": "#/components/schemas/EnvelopeCreate",
"description": "Email envelope information"
}
},
"required": ["mailbox", "message", "envelope"]
},
"EmailUpdate": {
"type": "object",
"properties": {
"mailbox": {
"type": "string",
"description": "Mailbox to move the email to"
},
"message": {
"type": "string",
"description": "Updated content of the email"
},
"attachments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Attachment"
},
"description": "Updated list of attachments"
},
"flags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Updated email flags"
},
"envelope": {
"$ref": "#/components/schemas/EnvelopeCreate",
"description": "Updated email envelope information"
}
}
},
"Attachment": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "Name of the attachment file"
},
"content_type": {
"type": "string",
"description": "MIME type of the attachment"
},
"data": {
"type": "string",
"description": "Base64 encoded attachment data"
}
},
"required": ["filename", "content_type", "data"]
},
"Envelope": {
"type": "object",
"properties": {
"date": {
"type": "integer",
"format": "int64",
"description": "Date of the email (Unix timestamp)"
},
"subject": {
"type": "string",
"description": "Subject of the email"
},
"from": {
"type": "array",
"items": {
"type": "string"
},
"description": "From addresses"
},
"sender": {
"type": "array",
"items": {
"type": "string"
},
"description": "Sender addresses"
},
"reply_to": {
"type": "array",
"items": {
"type": "string"
},
"description": "Reply-To addresses"
},
"to": {
"type": "array",
"items": {
"type": "string"
},
"description": "To addresses"
},
"cc": {
"type": "array",
"items": {
"type": "string"
},
"description": "CC addresses"
},
"bcc": {
"type": "array",
"items": {
"type": "string"
},
"description": "BCC addresses"
},
"in_reply_to": {
"type": "string",
"description": "Message ID this email is replying to"
},
"message_id": {
"type": "string",
"description": "Unique message ID"
}
},
"required": ["date", "subject", "from", "to"]
},
"EnvelopeCreate": {
"type": "object",
"properties": {
"subject": {
"type": "string",
"description": "Subject of the email"
},
"from": {
"type": "array",
"items": {
"type": "string"
},
"description": "From addresses"
},
"to": {
"type": "array",
"items": {
"type": "string"
},
"description": "To addresses"
},
"cc": {
"type": "array",
"items": {
"type": "string"
},
"description": "CC addresses"
},
"bcc": {
"type": "array",
"items": {
"type": "string"
},
"description": "BCC addresses"
}
},
"required": ["subject", "from", "to"]
},
"UpdateEmailFlags": {
"type": "object",
"properties": {
"flags": {
"type": "array",
"items": {
"type": "string"
},
"description": "New flags to set on the email"
}
},
"required": ["flags"]
}
}
}
}

View File

@@ -35,4 +35,39 @@ for struct in structs {
}
```
The [openrpc/docgen](../openrpc/docgen/) module demonstrates a good use case, where codemodels are serialized into JSON schema's, to generate an OpenRPC description document from a client in v.
The [openrpc/docgen](../openrpc/docgen/) module demonstrates a good use case, where codemodels are serialized into JSON schema's, to generate an OpenRPC description document from a client in v.## V Language Utilities
The `vlang_utils.v` file provides a set of utility functions for working with V language files and code. These utilities are useful for:
1. **File Operations**
- `list_v_files(dir string) ![]string` - Lists all V files in a directory, excluding generated files
- `get_module_dir(mod string) string` - Converts a V module path to a directory path
2. **Code Inspection and Analysis**
- `get_function_from_file(file_path string, function_name string) !string` - Extracts a function definition from a file
- `get_function_from_module(module_path string, function_name string) !string` - Searches for a function across all files in a module
- `get_type_from_module(module_path string, type_name string) !string` - Searches for a type definition across all files in a module
3. **V Language Tools**
- `vtest(fullpath string) !string` - Runs V tests on files or directories
- `vvet(fullpath string) !string` - Runs V vet on files or directories
### Example Usage
```v
// Find and extract a function definition
function_def := code.get_function_from_module('/path/to/module', 'my_function') or {
eprintln('Could not find function: ${err}')
return
}
println(function_def)
// Run tests on a directory
test_results := code.vtest('/path/to/module') or {
eprintln('Tests failed: ${err}')
return
}
println(test_results)
```
These utilities are particularly useful when working with code generation, static analysis, or when building developer tools that need to inspect V code.

View File

@@ -1,5 +1,6 @@
module code
import log
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.core.pathlib
import os
@@ -8,7 +9,6 @@ pub interface IFile {
write(string, WriteOptions) !
write_str(WriteOptions) !string
name string
write(string, WriteOptions) !
}
pub struct File {
@@ -132,6 +132,7 @@ pub fn (code VFile) write_str(options WriteOptions) !string {
}
pub fn (file VFile) get_function(name string) ?Function {
log.error('Looking for function ${name} in file ${file.name}')
functions := file.items.filter(it is Function).map(it as Function)
target_lst := functions.filter(it.name == name)
@@ -161,3 +162,120 @@ pub fn (file VFile) functions() []Function {
pub fn (file VFile) structs() []Struct {
return file.items.filter(it is Struct).map(it as Struct)
}
// parse_vfile parses V code into a VFile struct
// It extracts the module name, imports, constants, structs, and functions
pub fn parse_vfile(code string) !VFile {
mut vfile := VFile{
content: code
}
lines := code.split_into_lines()
// Extract module name
for line in lines {
trimmed := line.trim_space()
if trimmed.starts_with('module ') {
vfile.mod = trimmed.trim_string_left('module ').trim_space()
break
}
}
// Extract imports
for line in lines {
trimmed := line.trim_space()
if trimmed.starts_with('import ') {
import_obj := parse_import(trimmed)
vfile.imports << import_obj
}
}
// Extract constants
vfile.consts = parse_consts(code) or { []Const{} }
// Split code into chunks for parsing structs and functions
mut chunks := []string{}
mut current_chunk := ''
mut brace_count := 0
mut in_struct_or_fn := false
mut comment_block := []string{}
for line in lines {
trimmed := line.trim_space()
// Collect comments
if trimmed.starts_with('//') && !in_struct_or_fn {
comment_block << line
continue
}
// Check for struct or function start
if (trimmed.starts_with('struct ') || trimmed.starts_with('pub struct ') ||
trimmed.starts_with('fn ') || trimmed.starts_with('pub fn ')) && !in_struct_or_fn {
in_struct_or_fn = true
current_chunk = comment_block.join('\n')
if current_chunk != '' {
current_chunk += '\n'
}
current_chunk += line
comment_block = []string{}
if line.contains('{') {
brace_count += line.count('{')
}
if line.contains('}') {
brace_count -= line.count('}')
}
if brace_count == 0 {
// Single line definition
chunks << current_chunk
current_chunk = ''
in_struct_or_fn = false
}
continue
}
// Add line to current chunk if we're inside a struct or function
if in_struct_or_fn {
current_chunk += '\n' + line
if line.contains('{') {
brace_count += line.count('{')
}
if line.contains('}') {
brace_count -= line.count('}')
}
// Check if we've reached the end of the struct or function
if brace_count == 0 {
chunks << current_chunk
current_chunk = ''
in_struct_or_fn = false
}
}
}
// Parse each chunk and add to items
for chunk in chunks {
trimmed := chunk.trim_space()
if trimmed.contains('struct ') || trimmed.contains('pub struct ') {
// Parse struct
struct_obj := parse_struct(chunk) or {
// Skip invalid structs
continue
}
vfile.items << struct_obj
} else if trimmed.contains('fn ') || trimmed.contains('pub fn ') {
// Parse function
fn_obj := parse_function(chunk) or {
// Skip invalid functions
continue
}
vfile.items << fn_obj
}
}
return vfile
}

View File

@@ -0,0 +1,87 @@
module code
fn test_parse_vfile() {
code := '
module test
import os
import strings
import freeflowuniverse.herolib.core.texttools
const (
VERSION = \'1.0.0\'
DEBUG = true
)
pub struct Person {
pub mut:
name string
age int
}
// greet returns a greeting message
pub fn (p Person) greet() string {
return \'Hello, my name is \${p.name} and I am \${p.age} years old\'
}
// create_person creates a new Person instance
pub fn create_person(name string, age int) Person {
return Person{
name: name
age: age
}
}
'
vfile := parse_vfile(code) or {
assert false, 'Failed to parse VFile: ${err}'
return
}
// Test module name
assert vfile.mod == 'test'
// Test imports
assert vfile.imports.len == 3
assert vfile.imports[0].mod == 'os'
assert vfile.imports[1].mod == 'strings'
assert vfile.imports[2].mod == 'freeflowuniverse.herolib.core.texttools'
// Test constants
assert vfile.consts.len == 2
assert vfile.consts[0].name == 'VERSION'
assert vfile.consts[0].value == '\'1.0.0\''
assert vfile.consts[1].name == 'DEBUG'
assert vfile.consts[1].value == 'true'
// Test structs
structs := vfile.structs()
assert structs.len == 1
assert structs[0].name == 'Person'
assert structs[0].is_pub == true
assert structs[0].fields.len == 2
assert structs[0].fields[0].name == 'name'
assert structs[0].fields[0].typ.vgen() == 'string'
assert structs[0].fields[1].name == 'age'
assert structs[0].fields[1].typ.vgen() == 'int'
// Test functions
functions := vfile.functions()
assert functions.len == 2
// Test method
assert functions[0].name == 'greet'
assert functions[0].is_pub == true
assert functions[0].receiver.typ.vgen() == 'Person'
assert functions[0].result.typ.vgen() == 'string'
// Test standalone function
assert functions[1].name == 'create_person'
assert functions[1].is_pub == true
assert functions[1].params.len == 2
assert functions[1].params[0].name == 'name'
assert functions[1].params[0].typ.vgen() == 'string'
assert functions[1].params[1].name == 'age'
assert functions[1].params[1].typ.vgen() == 'int'
assert functions[1].result.typ.vgen() == 'Person'
}

View File

@@ -43,35 +43,28 @@ pub fn (mod Module) write(path string, options WriteOptions) ! {
}
for file in mod.files {
console.print_debug('mod file write ${file.name}')
file.write(module_dir.path, options)!
}
for folder in mod.folders {
console.print_debug('mod folder write ${folder.name}')
folder.write('${path}/${mod.name}', options)!
}
for mod_ in mod.modules {
console.print_debug('mod write ${mod_.name}')
mod_.write('${path}/${mod.name}', options)!
}
if options.format {
console.print_debug('format ${module_dir.path}')
os.execute('v fmt -w ${module_dir.path}')
}
if options.compile {
console.print_debug('compile shared ${module_dir.path}')
os.execute_opt('${pre} -shared ${module_dir.path}') or { log.fatal(err.msg()) }
}
if options.test {
console.print_debug('test ${module_dir.path}')
os.execute_opt('${pre} test ${module_dir.path}') or { log.fatal(err.msg()) }
}
if options.document {
docs_path := '${path}/${mod.name}/docs'
console.print_debug('document ${module_dir.path}')
os.execute('v doc -f html -o ${docs_path} ${module_dir.path}')
}
@@ -82,7 +75,6 @@ pub fn (mod Module) write(path string, options WriteOptions) ! {
pub fn (mod Module) write_str() !string {
mut out := ''
for file in mod.files {
console.print_debug("mod file write ${file.name}")
out += file.write_str()!
}

274
lib/core/code/vlang_utils.v Normal file
View File

@@ -0,0 +1,274 @@
module code
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.mcp.logger
import os
import log
// ===== FILE AND DIRECTORY OPERATIONS =====
// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v
// ARGS:
// dir string - directory path to search
// RETURNS:
// []string - list of absolute paths to V files
pub fn list_v_files(dir string) ![]string {
files := os.ls(dir) or { return error('Error listing directory: ${err}') }
mut v_files := []string{}
for file in files {
if file.ends_with('.v') && !file.ends_with('_.v') {
filepath := os.join_path(dir, file)
v_files << filepath
}
}
return v_files
}
// get_module_dir converts a V module path to a directory path
// ARGS:
// mod string - module name (e.g., 'freeflowuniverse.herolib.mcp')
// RETURNS:
// string - absolute path to the module directory
pub fn get_module_dir(mod string) string {
module_parts := mod.trim_string_left('freeflowuniverse.herolib').split('.')
return '${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/${module_parts.join('/')}'
}
// ===== CODE PARSING UTILITIES =====
// find_closing_brace finds the position of the closing brace that matches an opening brace
// ARGS:
// content string - the string to search in
// start_i int - the position after the opening brace
// RETURNS:
// ?int - position of the matching closing brace, or none if not found
fn find_closing_brace(content string, start_i int) ?int {
mut brace_count := 1
for i := start_i; i < content.len; i++ {
if content[i] == `{` {
brace_count++
} else if content[i] == `}` {
brace_count--
if brace_count == 0 {
return i
}
}
}
return none
}
// get_function_from_file parses a V file and extracts a specific function block including its comments
// ARGS:
// file_path string - path to the V file
// function_name string - name of the function to extract
// RETURNS:
// string - the function block including comments, or error if not found
pub fn get_function_from_file(file_path string, function_name string) !Function {
content := os.read_file(file_path) or { return error('Failed to read file ${file_path}: ${err}') }
vfile := parse_vfile(content) or { return error('Failed to parse file ${file_path}: ${err}') }
if fn_obj := vfile.get_function(function_name) {
return fn_obj
}
return error('function ${function_name} not found in file ${file_path}')
}
// get_function_from_module searches for a function in all V files within a module
// ARGS:
// module_path string - path to the module directory
// function_name string - name of the function to find
// RETURNS:
// string - the function definition if found, or error if not found
pub fn get_function_from_module(module_path string, function_name string) !Function {
v_files := list_v_files(module_path) or {
return error('Failed to list V files in ${module_path}: ${err}')
}
log.error('Found ${v_files} V files in ${module_path}')
for v_file in v_files {
// Read the file content
content := os.read_file(v_file) or {
continue
}
// Parse the file
vfile := parse_vfile(content) or {
continue
}
// Look for the function
if fn_obj := vfile.get_function(function_name) {
return fn_obj
}
}
return error('function ${function_name} not found in module ${module_path}')
}
// get_type_from_module searches for a type definition in all V files within a module
// ARGS:
// module_path string - path to the module directory
// type_name string - name of the type to find
// RETURNS:
// string - the type definition if found, or error if not found
pub fn get_type_from_module(module_path string, type_name string) !string {
println('Looking for type ${type_name} in module ${module_path}')
v_files := list_v_files(module_path) or {
return error('Failed to list V files in ${module_path}: ${err}')
}
for v_file in v_files {
println('Checking file: ${v_file}')
content := os.read_file(v_file) or { return error('Failed to read file ${v_file}: ${err}') }
// Look for both regular and pub struct declarations
mut type_str := 'struct ${type_name} {'
mut i := content.index(type_str) or { -1 }
mut is_pub := false
if i == -1 {
// Try with pub struct
type_str = 'pub struct ${type_name} {'
i = content.index(type_str) or { -1 }
is_pub = true
}
if i == -1 {
type_import := content.split_into_lines().filter(it.contains('import')
&& it.contains(type_name))
if type_import.len > 0 {
log.debug('debugzoooo')
mod := type_import[0].trim_space().trim_string_left('import ').all_before(' ')
return get_type_from_module(get_module_dir(mod), type_name)
}
continue
}
println('Found type ${type_name} in ${v_file} at position ${i}')
// Find the start of the struct definition including comments
mut comment_start := i
mut line_start := i
// Find the start of the line containing the struct definition
for j := i; j >= 0; j-- {
if j == 0 || content[j - 1] == `\n` {
line_start = j
break
}
}
// Find the start of the comment block (if any)
for j := line_start - 1; j >= 0; j-- {
if j == 0 {
comment_start = 0
break
}
// If we hit a blank line or a non-comment line, stop
if content[j] == `\n` {
if j > 0 && j < content.len - 1 {
// Check if the next line starts with a comment
next_line_start := j + 1
if next_line_start < content.len && content[next_line_start] != `/` {
comment_start = j + 1
break
}
}
}
}
// Find the end of the struct definition
closing_i := find_closing_brace(content, i + type_str.len) or {
return error('could not find where declaration for type ${type_name} ends')
}
// Get the full struct definition including the struct declaration line
full_struct := content.substr(line_start, closing_i + 1)
println('Found struct definition:\n${full_struct}')
// Return the full struct definition
return full_struct
}
return error('type ${type_name} not found in module ${module_path}')
}
// ===== V LANGUAGE TOOLS =====
// vtest runs v test on the specified file or directory
// ARGS:
// fullpath string - path to the file or directory to test
// RETURNS:
// string - test results output, or error if test fails
pub fn vtest(fullpath string) !string {
logger.info('test ${fullpath}')
if !os.exists(fullpath) {
return error('File or directory does not exist: ${fullpath}')
}
if os.is_dir(fullpath) {
mut results := ''
for item in list_v_files(fullpath)! {
results += vtest(item)!
results += '\n-----------------------\n'
}
return results
} else {
cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}'
logger.debug('Executing command: ${cmd}')
result := os.execute(cmd)
if result.exit_code != 0 {
return error('Test failed for ${fullpath} with exit code ${result.exit_code}\n${result.output}')
} else {
logger.info('Test completed for ${fullpath}')
}
return 'Command: ${cmd}\nExit code: ${result.exit_code}\nOutput:\n${result.output}'
}
}
// vet_file runs v vet on a single file
// ARGS:
// file string - path to the file to vet
// RETURNS:
// string - vet results output, or error if vet fails
fn vet_file(file string) !string {
cmd := 'v vet -v -w ${file}'
logger.debug('Executing command: ${cmd}')
result := os.execute(cmd)
if result.exit_code != 0 {
return error('Vet failed for ${file} with exit code ${result.exit_code}\n${result.output}')
} else {
logger.info('Vet completed for ${file}')
}
return 'Command: ${cmd}\nExit code: ${result.exit_code}\nOutput:\n${result.output}'
}
// vvet runs v vet on the specified file or directory
// ARGS:
// fullpath string - path to the file or directory to vet
// RETURNS:
// string - vet results output, or error if vet fails
pub fn vvet(fullpath string) !string {
logger.info('vet ${fullpath}')
if !os.exists(fullpath) {
return error('File or directory does not exist: ${fullpath}')
}
if os.is_dir(fullpath) {
mut results := ''
files := list_v_files(fullpath) or { return error('Error listing V files: ${err}') }
for file in files {
results += vet_file(file) or {
logger.error('Failed to vet ${file}: ${err}')
return error('Failed to vet ${file}: ${err}')
}
results += '\n-----------------------\n'
}
return results
} else {
return vet_file(fullpath)
}
}

View File

@@ -4,42 +4,77 @@ This module provides a V language implementation of the [Model Context Protocol
## Overview
The MCP module implements a server that communicates using the Standard Input/Output (stdio) transport as described in the [MCP transport specification](https://modelcontextprotocol.io/docs/concepts/transports). This allows for standardized communication between AI models and tools or applications that provide context to these models.
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.
## 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
- **Handlers**: Implementation of MCP protocol handlers, including initialization
- **Factory**: Functions to create and configure an MCP server
- **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
- Full implementation of the MCP protocol version 2024-11-05
- JSON-RPC based communication
- 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
- Resource management
- Minimal implementation requirements for server developers
## Usage
To create a new MCP server:
To create a new MCP server using the core module:
```v
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.schemas.jsonrpc
// Define custom handlers if needed
handlers := {
'custom_method': my_custom_handler
// 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 server configuration
config := mcp.ServerConfiguration{
// Configure server capabilities as needed
// 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'
}
}
})!
// Create and start the server
mut server := mcp.new_server(handlers, config)!
// Start the server
server.start()!
```
## Sub-modules
The MCP directory contains several sub-modules that implement specific MCP servers:
- **baobab**: An MCP server implementation for Baobab-specific tools and functionality
- **developer**: An MCP server implementation focused on developer tools
Each sub-module leverages the core MCP implementation but provides its own specific tools, handlers, and configurations. Thanks to the boilerplate functionality provided by the core module, these implementations only need to define their specific tools and handlers without worrying about the underlying protocol details.
## Dependencies
- `freeflowuniverse.herolib.schemas.jsonrpc`: For JSON-RPC communication
- `x.json2`: For JSON serialization/deserialization
- Standard V libraries: `time`, `os`, `log`
## License
This module is part of the HeroLib project. See the project's license for more information.

View File

@@ -19,6 +19,7 @@ pub mut:
tool_handlers map[string]ToolHandler
}
pub type ToolHandler = fn (arguments map[string]json2.Any) !ToolCallResult
fn (b &MemoryBackend) resource_exists(uri string) !bool {
return uri in b.resources
}
@@ -130,7 +131,7 @@ fn (b &MemoryBackend) tool_call(name string, arguments map[string]json2.Any) !To
content: [
ToolContent{
typ: 'text'
text: 'Error: ${err.msg}'
text: 'Error: ${err.msg()}'
},
]
}

3
lib/mcp/baobab/README.md Normal file
View File

@@ -0,0 +1,3 @@
## Baobab MCP
The Base Object and Actor Backend MCP Server provides tools to easily generate BaObAB modules for a given OpenAPI or OpenRPC Schema.

View File

@@ -1,40 +1,167 @@
module baobab
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.schemas.jsonschema
import freeflowuniverse.herolib.core.code
import x.json2 as json { Any }
import freeflowuniverse.herolib.mcp.logger
import freeflowuniverse.herolib.baobab.generator
import freeflowuniverse.herolib.baobab.specification
// generate_methods_file MCP Tool
//
const generate_methods_file_tool = mcp.Tool{
name: 'generate_methods_file'
description: 'Generates a methods file with methods for a backend corresponding to thos specified in an OpenAPI or OpenRPC specification'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {'source': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'openapi_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'openrpc_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
})}
required: ['source']
}
}
pub fn (d &Baobab) generate_methods_file_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
source := json.decode[generator.Source](arguments["source"].str())!
result := generator.generate_methods_file_str(source)
or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: mcp.result_to_mcp_tool_contents[string](result)
}
}
// generate_module_from_openapi MCP Tool
const generate_module_from_openapi_tool = mcp.Tool{
name: 'generate_module_from_openapi'
description: ''
input_schema: mcp.ToolInputSchema{
input_schema: jsonschema.Schema{
typ: 'object'
properties: {
'openapi_path': mcp.ToolProperty{
properties: {'openapi_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
items: mcp.ToolItems{
typ: ''
enum: []
}
enum: []
}
}
})}
required: ['openapi_path']
}
}
pub fn generate_module_from_openapi_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
println('debugzo31')
openapi_path := arguments['openapi_path'].str()
println('debugzo32')
result := generator.generate_module_from_openapi(openapi_path) or {
println('debugzo33')
pub fn (d &Baobab) generate_module_from_openapi_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
openapi_path := arguments["openapi_path"].str()
result := generator.generate_module_from_openapi(openapi_path)
or {
return mcp.error_tool_call_result(err)
}
println('debugzo34')
return mcp.ToolCallResult{
is_error: false
content: result_to_mcp_tool_contents[string](result)
content: mcp.result_to_mcp_tool_contents[string](result)
}
}
// generate_methods_interface_file MCP Tool
const generate_methods_interface_file_tool = mcp.Tool{
name: 'generate_methods_interface_file'
description: 'Generates a methods interface file with method interfaces for a backend corresponding to those specified in an OpenAPI or OpenRPC specification'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {'source': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'openapi_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'openrpc_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
})}
required: ['source']
}
}
pub fn (d &Baobab) generate_methods_interface_file_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
source := json.decode[generator.Source](arguments["source"].str())!
result := generator.generate_methods_interface_file_str(source)
or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: mcp.result_to_mcp_tool_contents[string](result)
}
}
// generate_model_file MCP Tool
const generate_model_file_tool = mcp.Tool{
name: 'generate_model_file'
description: 'Generates a model file with data structures for a backend corresponding to those specified in an OpenAPI or OpenRPC specification'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {'source': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'openapi_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'openrpc_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
})}
required: ['source']
}
}
pub fn (d &Baobab) generate_model_file_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
source := json.decode[generator.Source](arguments["source"].str())!
result := generator.generate_model_file_str(source)
or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: mcp.result_to_mcp_tool_contents[string](result)
}
}
// generate_methods_example_file MCP Tool
const generate_methods_example_file_tool = mcp.Tool{
name: 'generate_methods_example_file'
description: 'Generates a methods example file with example implementations for a backend corresponding to those specified in an OpenAPI or OpenRPC specification'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {'source': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'openapi_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'openrpc_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
})}
required: ['source']
}
}
pub fn (d &Baobab) generate_methods_example_file_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
source := json.decode[generator.Source](arguments["source"].str())!
result := generator.generate_methods_example_file_str(source)
or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: mcp.result_to_mcp_tool_contents[string](result)
}
}

View File

@@ -0,0 +1,101 @@
module baobab
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.schemas.jsonrpc
import json
import x.json2
import os
// This file contains tests for the Baobab tools implementation.
// It tests the tools' ability to handle tool calls and return expected results.
// test_generate_module_from_openapi_tool tests the generate_module_from_openapi tool definition
fn test_generate_module_from_openapi_tool() {
// Verify the tool definition
assert generate_module_from_openapi_tool.name == 'generate_module_from_openapi', 'Tool name should be "generate_module_from_openapi"'
// Verify the input schema
assert generate_module_from_openapi_tool.input_schema.typ == 'object', 'Input schema type should be "object"'
assert 'openapi_path' in generate_module_from_openapi_tool.input_schema.properties, 'Input schema should have "openapi_path" property'
assert generate_module_from_openapi_tool.input_schema.properties['openapi_path'].typ == 'string', 'openapi_path property should be of type "string"'
assert 'openapi_path' in generate_module_from_openapi_tool.input_schema.required, 'openapi_path should be a required property'
}
// test_generate_module_from_openapi_tool_handler_error tests the error handling of the generate_module_from_openapi tool handler
fn test_generate_module_from_openapi_tool_handler_error() {
// Create arguments with a non-existent file path
mut arguments := map[string]json2.Any{}
arguments['openapi_path'] = json2.Any('non_existent_file.yaml')
// Call the handler
result := generate_module_from_openapi_tool_handler(arguments) or {
// If the handler returns an error, that's expected
assert err.msg().contains(''), 'Error message should not be empty'
return
}
// If we get here, the handler should have returned an error result
assert result.is_error, 'Result should indicate an error'
assert result.content.len > 0, 'Error content should not be empty'
assert result.content[0].typ == 'text', 'Error content should be of type "text"'
assert result.content[0].text.contains('failed to open file'), 'Error content should contain "failed to open file", instead ${result.content[0].text}'
}
// test_mcp_tool_call_integration tests the integration of the tool with the MCP server
fn test_mcp_tool_call_integration() {
// Create a new MCP server
mut server := new_mcp_server() or {
assert false, 'Failed to create MCP server: ${err}'
return
}
// Create a temporary OpenAPI file for testing
temp_dir := os.temp_dir()
temp_file := os.join_path(temp_dir, 'test_openapi.yaml')
os.write_file(temp_file, 'openapi: 3.0.0\ninfo:\n title: Test API\n version: 1.0.0\npaths:\n /test:\n get:\n summary: Test endpoint\n responses:\n "200":\n description: OK') or {
assert false, 'Failed to create temporary file: ${err}'
return
}
// Sample tool call request
tool_call_request := '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"generate_module_from_openapi","arguments":{"openapi_path":"${temp_file}"}}}'
// Process the request through the handler
response := server.handler.handle(tool_call_request) or {
// Clean up the temporary file
os.rm(temp_file) or {}
// If the handler returns an error, that's expected in this test environment
// since we might not have all dependencies set up
return
}
// Clean up the temporary file
os.rm(temp_file) or {}
// Decode the response to verify its structure
decoded_response := jsonrpc.decode_response(response) or {
// In a test environment, we might get an error due to missing dependencies
// This is acceptable for this test
return
}
// If we got a successful response, verify it
if !decoded_response.is_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 to check the content
result_map := json2.raw_decode(result_json) or {
assert false, 'Failed to decode result: ${err}'
return
}.as_map()
// Verify the result structure
assert 'isError' in result_map, 'Result should have isError field'
assert 'content' in result_map, 'Result should have content field'
}
}

23
lib/mcp/baobab/command.v Normal file
View File

@@ -0,0 +1,23 @@
module baobab
import cli
pub const command := cli.Command{
sort_flags: true
name: 'baobab'
// execute: cmd_mcpgen
description: 'baobab command'
commands: [
cli.Command{
name: 'start'
execute: cmd_start
description: 'start the Baobab server'
}
]
}
fn cmd_start(cmd cli.Command) ! {
mut server := new_mcp_server(&Baobab{})!
server.start()!
}

View File

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

128
lib/mcp/baobab/mcp_test.v Normal file
View File

@@ -0,0 +1,128 @@
module baobab
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.schemas.jsonrpc
import json
import x.json2
// This file contains tests for the Baobab MCP server implementation.
// It tests the server's ability to initialize and handle tool calls.
// test_new_mcp_server tests the creation of a new MCP server for the Baobab module
fn test_new_mcp_server() {
// Create a new MCP server
mut server := new_mcp_server() or {
assert false, 'Failed to create MCP server: ${err}'
return
}
// Verify server info
assert server.server_info.name == 'developer', 'Server name should be "developer"'
assert server.server_info.version == '1.0.0', 'Server version should be 1.0.0'
// Verify server capabilities
assert server.capabilities.prompts.list_changed == true, 'Prompts capability should have list_changed set to true'
assert server.capabilities.resources.subscribe == true, 'Resources capability should have subscribe set to true'
assert server.capabilities.resources.list_changed == true, 'Resources capability should have list_changed set to true'
assert server.capabilities.tools.list_changed == true, 'Tools capability should have list_changed set to true'
}
// test_mcp_server_initialize tests the initialize handler with a sample initialize request
fn test_mcp_server_initialize() {
// Create a new MCP server
mut server := new_mcp_server() or {
assert false, 'Failed to create MCP server: ${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 := server.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(mcp.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 info
assert result.server_info.name == 'developer', 'Server name should be "developer"'
}
// test_tools_list tests the tools/list handler to verify tool registration
fn test_tools_list() {
// Create a new MCP server
mut server := new_mcp_server() or {
assert false, 'Failed to create MCP server: ${err}'
return
}
// Sample tools/list request
tools_list_request := '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"cursor":""}}'
// Process the request through the handler
response := server.handler.handle(tools_list_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 a map to check the tools
result_map := json2.raw_decode(result_json) or {
assert false, 'Failed to decode result: ${err}'
return
}.as_map()
// Verify that the tools array exists and contains the expected tool
tools := result_map['tools'].arr()
assert tools.len > 0, 'Tools list should not be empty'
// Find the generate_module_from_openapi tool
mut found_tool := false
for tool in tools {
tool_map := tool.as_map()
if tool_map['name'].str() == 'generate_module_from_openapi' {
found_tool = true
break
}
}
assert found_tool, 'generate_module_from_openapi tool should be registered'
}

38
lib/mcp/baobab/server.v Normal file
View File

@@ -0,0 +1,38 @@
module baobab
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.mcp.logger
import freeflowuniverse.herolib.schemas.jsonrpc
@[heap]
pub struct Baobab {}
pub fn new_mcp_server(v &Baobab) !&mcp.Server {
logger.info('Creating new Baobab MCP server')
// Initialize the server with the empty handlers map
mut server := mcp.new_server(mcp.MemoryBackend{
tools: {
'generate_module_from_openapi': generate_module_from_openapi_tool
'generate_methods_file': generate_methods_file_tool
'generate_methods_interface_file': generate_methods_interface_file_tool
'generate_model_file': generate_model_file_tool
'generate_methods_example_file': generate_methods_example_file_tool
}
tool_handlers: {
'generate_module_from_openapi': v.generate_module_from_openapi_tool_handler
'generate_methods_file': v.generate_methods_file_tool_handler
'generate_methods_interface_file': v.generate_methods_interface_file_tool_handler
'generate_model_file': v.generate_model_file_tool_handler
'generate_methods_example_file': v.generate_methods_example_file_tool_handler
}
}, mcp.ServerParams{
config: mcp.ServerConfiguration{
server_info: mcp.ServerInfo{
name: 'baobab'
version: '1.0.0'
}
}
})!
return server
}

View File

@@ -1,11 +0,0 @@
module mcp
pub fn error_tool_call_result(err IError) ToolCallResult {
return ToolCallResult{
is_error: true
content: [ToolContent{
typ: 'text'
text: err.msg()
}]
}
}

View File

@@ -14,37 +14,37 @@ pub:
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,
// }
// 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,
// 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,
// 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,
// Prompt handlers
'prompts/list': server.prompts_list_handler,
'prompts/get': server.prompts_get_handler,
// // Tool handlers
// 'tools/list': server.tools_list_handler,
// 'tools/call': server.tools_call_handler
// }
// })!
// Tool handlers
'tools/list': server.tools_list_handler,
'tools/call': server.tools_call_handler
}
})!
// server.handler = *handler
// return server
// }
server.handler = *handler
return server
}

View File

@@ -1 +1,53 @@
module mcp
pub fn result_to_mcp_tool_contents[T](result T) []ToolContent {
return [result_to_mcp_tool_content(result)]
}
pub fn result_to_mcp_tool_content[T](result T) ToolContent {
return $if T is string {
ToolContent{
typ: 'text'
text: result.str()
}
} $else $if T is int {
ToolContent{
typ: 'number'
number: result.int()
}
} $else $if T is bool {
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

@@ -6,6 +6,7 @@ import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.schemas.jsonschema
import freeflowuniverse.herolib.mcp.logger
// Tool related structs
@@ -14,14 +15,7 @@ pub struct Tool {
pub:
name string
description string
input_schema ToolInputSchema @[json: 'inputSchema']
}
pub struct ToolInputSchema {
pub:
typ string @[json: 'type']
properties map[string]ToolProperty
required []string
input_schema jsonschema.Schema @[json: 'inputSchema']
}
pub struct ToolProperty {
@@ -35,6 +29,7 @@ pub struct ToolItems {
pub:
typ string @[json: 'type']
enum []string
properties map[string]ToolProperty
}
pub struct ToolContent {
@@ -69,12 +64,15 @@ fn (mut s Server) tools_list_handler(data string) !string {
// TODO: Implement pagination logic using the cursor
// For now, return all tools
// Create a success response with the result
response := jsonrpc.new_response_generic[ToolListResult](request.id, ToolListResult{
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()
}
@@ -96,10 +94,8 @@ pub:
// 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 {
println('debugzo301')
// Decode the request with name and arguments parameters
request_map := json2.raw_decode(data)!.as_map()
println('debugzo30')
params_map := request_map['params'].as_map()
tool_name := params_map['name'].str()
if !s.backend.tool_exists(tool_name)! {
@@ -118,9 +114,11 @@ fn (mut s Server) tools_call_handler(data string) !string {
}
}
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)
@@ -142,3 +140,13 @@ pub fn (mut s Server) send_tools_list_changed_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()
}]
}
}

92
lib/mcp/mcpgen/README.md Normal file
View File

@@ -0,0 +1,92 @@
# MCP Generator
An implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for V language operations. This server uses the Standard Input/Output (stdio) transport as described in the [MCP documentation](https://modelcontextprotocol.io/docs/concepts/transports).
## Features
The server supports the following operations:
1. **test** - Run V tests on a file or directory
2. **run** - Execute V code from a file or directory
3. **compile** - Compile V code from a file or directory
4. **vet** - Run V vet on a file or directory
## Usage
### Building the Server
```bash
v -gc none -stats -enable-globals -n -w -cg -g -cc tcc /Users/despiegk/code/github/freeflowuniverse/herolib/lib/mcp/v_do
```
### Using the Server
The server communicates using the MCP protocol over stdio. To send a request, use the following format:
```
Content-Length: <length>
{"jsonrpc":"2.0","id":"<request-id>","method":"<method-name>","params":{"fullpath":"<path-to-file-or-directory>"}}
```
Where:
- `<length>` is the length of the JSON message in bytes
- `<request-id>` is a unique identifier for the request
- `<method-name>` is one of: `test`, `run`, `compile`, or `vet`
- `<path-to-file-or-directory>` is the absolute path to the V file or directory to process
### Example
Request:
```
Content-Length: 85
{"jsonrpc":"2.0","id":"1","method":"test","params":{"fullpath":"/path/to/file.v"}}
```
Response:
```
Content-Length: 245
{"jsonrpc":"2.0","id":"1","result":{"output":"Command: v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test /path/to/file.v\nExit code: 0\nOutput:\nAll tests passed!"}}
```
## Methods
### test
Runs V tests on the specified file or directory.
Command used:
```
v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}
```
If a directory is specified, it will run tests on all `.v` files in the directory (non-recursive).
### run
Executes the specified V file or all V files in a directory.
Command used:
```
v -gc none -stats -enable-globals -n -w -cg -g -cc tcc run ${fullpath}
```
### compile
Compiles the specified V file or all V files in a directory.
Command used:
```
cd /tmp && v -gc none -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath}
```
### vet
Runs V vet on the specified file or directory.
Command used:
```
v vet -v -w ${fullpath}
```

23
lib/mcp/mcpgen/command.v Normal file
View File

@@ -0,0 +1,23 @@
module mcpgen
import cli
pub const command := cli.Command{
sort_flags: true
name: 'mcpgen'
// execute: cmd_mcpgen
description: 'will list existing mdbooks'
commands: [
cli.Command{
name: 'start'
execute: cmd_start
description: 'start the MCP server'
}
]
}
fn cmd_start(cmd cli.Command) ! {
mut server := new_mcp_server(&MCPGen{})!
server.start()!
}

283
lib/mcp/mcpgen/mcpgen.v Normal file
View File

@@ -0,0 +1,283 @@
module mcpgen
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.schemas.jsonschema
import freeflowuniverse.herolib.schemas.jsonschema.codegen
import os
pub struct FunctionPointer {
name string // name of function
module_path string // path to module
}
// create_mcp_tool_code receives the name of a V language function string, and the path to the module in which it exists.
// returns an MCP Tool code in v for attaching the function to the mcp server
// function_pointers: A list of function pointers to generate tools for
pub fn (d &MCPGen) create_mcp_tools_code(function_pointers []FunctionPointer) !string {
mut str := ""
for function_pointer in function_pointers {
str += d.create_mcp_tool_code(function_pointer.name, function_pointer.module_path)!
}
return str
}
// create_mcp_tool_code receives the name of a V language function string, and the path to the module in which it exists.
// returns an MCP Tool code in v for attaching the function to the mcp server
pub fn (d &MCPGen) create_mcp_tool_code(function_name string, module_path string) !string {
if !os.exists(module_path) {
return error('Module path does not exist: ${module_path}')
}
function := code.get_function_from_module(module_path, function_name) or {
return error('Failed to get function ${function_name} from module ${module_path}\n${err}')
}
mut types := map[string]string{}
for param in function.params {
// Check if the type is an Object (struct)
if param.typ is code.Object {
types[param.typ.symbol()] = code.get_type_from_module(module_path, param.typ.symbol())!
}
}
// Get the result type if it's a struct
mut result_ := ""
if function.result.typ is code.Result {
result_type := (function.result.typ as code.Result).typ
if result_type is code.Object {
result_ = code.get_type_from_module(module_path, result_type.symbol())!
}
} else if function.result.typ is code.Object {
result_ = code.get_type_from_module(module_path, function.result.typ.symbol())!
}
tool_name := function.name
tool := d.create_mcp_tool(function, types)!
handler := d.create_mcp_tool_handler(function, types, result_)!
str := $tmpl('./templates/tool_code.v.template')
return str
}
// create_mcp_tool parses a V language function string and returns an MCP Tool struct
// function: The V function string including preceding comments
// types: A map of struct names to their definitions for complex parameter types
// result: The type of result of the create_mcp_tool function. Could be simply string, or struct {...}
pub fn (d &MCPGen) create_mcp_tool_handler(function code.Function, types map[string]string, result_ string) !string {
decode_stmts := function.params.map(argument_decode_stmt(it)).join_lines()
function_call := 'd.${function.name}(${function.params.map(it.name).join(',')})'
result := code.parse_type(result_)
str := $tmpl('./templates/tool_handler.v.template')
return str
}
pub fn argument_decode_stmt(param code.Param) string {
return if param.typ is code.Integer {
'${param.name} := arguments["${param.name}"].int()'
} else if param.typ is code.Boolean {
'${param.name} := arguments["${param.name}"].bool()'
} else if param.typ is code.String {
'${param.name} := arguments["${param.name}"].str()'
} else if param.typ is code.Object {
'${param.name} := json.decode[${param.typ.symbol()}](arguments["${param.name}"].str())!'
} else if param.typ is code.Array {
'${param.name} := json.decode[${param.typ.symbol()}](arguments["${param.name}"].str())!'
} else if param.typ is code.Map {
'${param.name} := json.decode[${param.typ.symbol()}](arguments["${param.name}"].str())!'
} else {
panic('Unsupported type: ${param.typ}')
}
}
/*
in @generate_mcp.v , implement a create_mpc_tool_handler function that given a vlang function string and the types that map to their corresponding type definitions (for instance struct some_type: SomeType{...}), generates a vlang function such as the following:
ou
pub fn (d &MCPGen) create_mcp_tool_tool_handler(arguments map[string]Any) !mcp.Tool {
function := arguments['function'].str()
types := json.decode[map[string]string](arguments['types'].str())!
return d.create_mcp_tool(function, types)
}
*/
// create_mcp_tool parses a V language function string and returns an MCP Tool struct
// function: The V function string including preceding comments
// types: A map of struct names to their definitions for complex parameter types
pub fn (d MCPGen) create_mcp_tool(function code.Function, types map[string]string) !mcp.Tool {
// Create input schema for parameters
mut properties := map[string]jsonschema.SchemaRef{}
mut required := []string{}
for param in function.params {
// Add to required parameters
required << param.name
// Create property for this parameter
mut property := jsonschema.SchemaRef{}
// Check if this is a complex type defined in the types map
if param.typ.symbol() in types {
// Parse the struct definition to create a nested schema
struct_def := types[param.typ.symbol()]
struct_schema := codegen.struct_to_schema(code.parse_struct(struct_def)!)
if struct_schema is jsonschema.Schema {
property = struct_schema
} else {
return error('Unsupported type: ${param.typ}')
}
} else {
// Handle primitive types
property = codegen.typesymbol_to_schema(param.typ.symbol())
}
properties[param.name] = property
}
// Create the input schema
input_schema := jsonschema.Schema{
typ: 'object',
properties: properties,
required: required
}
// Create and return the Tool
return mcp.Tool{
name: function.name,
description: function.description,
input_schema: input_schema
}
}
// // create_mcp_tool_input_schema creates a jsonschema.Schema for a given input type
// // input: The input type string
// // returns: A jsonschema.Schema for the given input type
// // errors: Returns an error if the input type is not supported
// pub fn (d MCPGen) create_mcp_tool_input_schema(input string) !jsonschema.Schema {
// // if input is a primitive type, return a mcp jsonschema.Schema with that type
// if input == 'string' {
// return jsonschema.Schema{
// typ: 'string'
// }
// } else if input == 'int' {
// return jsonschema.Schema{
// typ: 'integer'
// }
// } else if input == 'float' {
// return jsonschema.Schema{
// typ: 'number'
// }
// } else if input == 'bool' {
// return jsonschema.Schema{
// typ: 'boolean'
// }
// }
// // if input is a struct, return a mcp jsonschema.Schema with typ 'object' and properties for each field in the struct
// if input.starts_with('pub struct ') {
// struct_name := input[11..].split(' ')[0]
// fields := parse_struct_fields(input)
// mut properties := map[string]jsonschema.Schema{}
// for field_name, field_type in fields {
// property := jsonschema.Schema{
// typ: d.create_mcp_tool_input_schema(field_type)!.typ
// }
// properties[field_name] = property
// }
// return jsonschema.Schema{
// typ: 'object',
// properties: properties
// }
// }
// // if input is an array, return a mcp jsonschema.Schema with typ 'array' and items of the item type
// if input.starts_with('[]') {
// item_type := input[2..]
// // For array types, we create a schema with type 'array'
// // The actual item type is determined by the primitive type
// mut item_type_str := 'string' // default
// if item_type == 'int' {
// item_type_str = 'integer'
// } else if item_type == 'float' {
// item_type_str = 'number'
// } else if item_type == 'bool' {
// item_type_str = 'boolean'
// }
// // Create a property for the array items
// mut property := jsonschema.Schema{
// typ: 'array'
// }
// // Add the property to the schema
// mut properties := map[string]jsonschema.Schema{}
// properties['items'] = property
// return jsonschema.Schema{
// typ: 'array',
// properties: properties
// }
// }
// // Default to string type for unknown types
// return jsonschema.Schema{
// typ: 'string'
// }
// }
// parse_struct_fields parses a V language struct definition string and returns a map of field names to their types
fn parse_struct_fields(struct_def string) map[string]string {
mut fields := map[string]string{}
// Find the opening and closing braces of the struct definition
start_idx := struct_def.index('{') or { return fields }
end_idx := struct_def.last_index('}') or { return fields }
// Extract the content between the braces
struct_content := struct_def[start_idx + 1..end_idx].trim_space()
// Split the content by newlines to get individual field definitions
field_lines := struct_content.split('
')
for line in field_lines {
trimmed_line := line.trim_space()
// Skip empty lines and comments
if trimmed_line == '' || trimmed_line.starts_with('//') {
continue
}
// Handle pub: or mut: prefixes
mut field_def := trimmed_line
if field_def.starts_with('pub:') || field_def.starts_with('mut:') {
field_def = field_def.all_after(':').trim_space()
}
// Split by whitespace to separate field name and type
parts := field_def.split_any(' ')
if parts.len < 2 {
continue
}
field_name := parts[0]
field_type := parts[1..].join(' ')
// Handle attributes like @[json: 'name']
if field_name.contains('@[') {
continue
}
fields[field_name] = field_type
}
return fields
}

View File

@@ -1,7 +1,6 @@
module developer
module mcpgen
import freeflowuniverse.herolib.mcp
import x.json2
pub fn result_to_mcp_tool_contents[T](result T) []mcp.ToolContent {
return [result_to_mcp_tool_content(result)]

View File

@@ -0,0 +1,145 @@
module mcpgen
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.schemas.jsonschema
import x.json2 as json { Any }
// import json
// create_mcp_tools_code MCP Tool
// create_mcp_tool_code receives the name of a V language function string, and the path to the module in which it exists.
// returns an MCP Tool code in v for attaching the function to the mcp server
// function_pointers: A list of function pointers to generate tools for
const create_mcp_tools_code_tool = mcp.Tool{
name: 'create_mcp_tools_code'
description: 'create_mcp_tool_code receives the name of a V language function string, and the path to the module in which it exists.
returns an MCP Tool code in v for attaching the function to the mcp server
function_pointers: A list of function pointers to generate tools for'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {
'function_pointers': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'module_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['name', 'module_path']
}))
})
}
required: ['function_pointers']
}
}
pub fn (d &MCPGen) create_mcp_tools_code_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
function_pointers := json.decode[[]FunctionPointer](arguments["function_pointers"].str())!
result := d.create_mcp_tools_code(function_pointers)
or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: mcp.result_to_mcp_tool_contents[string](result)
}
}
const create_mcp_tool_code_tool = mcp.Tool{
name: 'create_mcp_tool_code'
description: 'create_mcp_tool_code receives the name of a V language function string, and the path to the module in which it exists.
returns an MCP Tool code in v for attaching the function to the mcp server'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {
'function_name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'module_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['function_name', 'module_path']
}
}
pub fn (d &MCPGen) create_mcp_tool_code_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
function_name := arguments['function_name'].str()
module_path := arguments['module_path'].str()
result := d.create_mcp_tool_code(function_name, module_path) or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: result_to_mcp_tool_contents[string](result)
}
}
// Tool definition for the create_mcp_tool function
const create_mcp_tool_const_tool = mcp.Tool{
name: 'create_mcp_tool_const'
description: 'Parses a V language function string and returns an MCP Tool struct. This tool analyzes function signatures, extracts parameters, and generates the appropriate MCP Tool representation.'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {
'function': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'types': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
})
}
required: ['function']
}
}
pub fn (d &MCPGen) create_mcp_tool_const_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
function := json.decode[code.Function](arguments['function'].str())!
types := json.decode[map[string]string](arguments['types'].str())!
result := d.create_mcp_tool(function, types) or { return mcp.error_tool_call_result(err) }
return mcp.ToolCallResult{
is_error: false
content: result_to_mcp_tool_contents[string](result.str())
}
}
// Tool definition for the create_mcp_tool_handler function
const create_mcp_tool_handler_tool = mcp.Tool{
name: 'create_mcp_tool_handler'
description: 'Generates a tool handler for the create_mcp_tool function. This tool handler accepts function string and types map and returns an MCP ToolCallResult.'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {
'function': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'types': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
})
'result': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['function', 'result']
}
}
// Tool handler for the create_mcp_tool_handler function
pub fn (d &MCPGen) create_mcp_tool_handler_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
function := json.decode[code.Function](arguments['function'].str())!
types := json.decode[map[string]string](arguments['types'].str())!
result_ := arguments['result'].str()
result := d.create_mcp_tool_handler(function, types, result_) or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: result_to_mcp_tool_contents[string](result)
}
}

View File

@@ -0,0 +1,21 @@
{
"type": "object",
"properties": {
"function_pointers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"module_path": {
"type": "string"
}
},
"required": ["name", "module_path"]
}
}
},
"required": ["function_pointers"]
}

35
lib/mcp/mcpgen/server.v Normal file
View File

@@ -0,0 +1,35 @@
module mcpgen
import freeflowuniverse.herolib.mcp.logger
import freeflowuniverse.herolib.mcp
@[heap]
pub struct MCPGen {}
pub fn new_mcp_server(v &MCPGen) !&mcp.Server {
logger.info('Creating new Developer MCP server')
// Initialize the server with the empty handlers map
mut server := mcp.new_server(mcp.MemoryBackend{
tools: {
'create_mcp_tool_code': create_mcp_tool_code_tool
'create_mcp_tool_const': create_mcp_tool_const_tool
'create_mcp_tool_handler': create_mcp_tool_handler_tool
'create_mcp_tools_code': create_mcp_tools_code_tool
}
tool_handlers: {
'create_mcp_tool_code': v.create_mcp_tool_code_tool_handler
'create_mcp_tool_const': v.create_mcp_tool_const_tool_handler
'create_mcp_tool_handler': v.create_mcp_tool_handler_tool_handler
'create_mcp_tools_code': v.create_mcp_tools_code_tool_handler
}
}, mcp.ServerParams{
config: mcp.ServerConfiguration{
server_info: mcp.ServerInfo{
name: 'mcpgen'
version: '1.0.0'
}
}
})!
return server
}

View File

@@ -1,4 +1,5 @@
// @{tool_name} MCP Tool
// @{tool.description}
const @{tool_name}_tool = @{tool.str()}

View File

@@ -0,0 +1,11 @@
pub fn (d &MCPGen) @{function.name}_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
@{decode_stmts}
result := @{function_call}
or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: mcp.result_to_mcp_tool_contents[@{result.symbol()}](result)
}
}

View File

@@ -0,0 +1 @@
@for import in

View File

@@ -1,6 +0,0 @@
module mcp
import x.json2
// ToolHandler is a function type that handles tool calls
pub type ToolHandler = fn (arguments map[string]json2.Any) !ToolCallResult

15
lib/mcp/vcode/command.v Normal file
View File

@@ -0,0 +1,15 @@
module vcode
import cli
pub const command := cli.Command{
sort_flags: true
name: 'vcode'
execute: cmd_vcode
description: 'will list existing mdbooks'
}
fn cmd_vcode(cmd cli.Command) ! {
mut server := new_mcp_server(&VCode{})!
server.start()!
}

View File

@@ -1,24 +1,30 @@
module baobab
module vcode
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.mcp.logger
import freeflowuniverse.herolib.schemas.jsonrpc
pub fn new_mcp_server() !&mcp.Server {
@[heap]
pub struct VCode {
v_version string = '0.1.0'
}
pub fn new_mcp_server(v &VCode) !&mcp.Server {
logger.info('Creating new Developer MCP server')
// Initialize the server with the empty handlers map
mut server := mcp.new_server(mcp.MemoryBackend{
tools: {
'generate_module_from_openapi': generate_module_from_openapi_tool
'get_function_from_file': get_function_from_file_tool
'write_vfile': write_vfile_tool
}
tool_handlers: {
'generate_module_from_openapi': generate_module_from_openapi_tool_handler
'get_function_from_file': v.get_function_from_file_tool_handler
'write_vfile': v.write_vfile_tool_handler
}
}, mcp.ServerParams{
config: mcp.ServerConfiguration{
server_info: mcp.ServerInfo{
name: 'developer'
name: 'vcode'
version: '1.0.0'
}
}

View File

@@ -1,4 +1,4 @@
module main
module vcode
import freeflowuniverse.herolib.mcp.logger
import freeflowuniverse.herolib.mcp

View File

@@ -1,4 +1,4 @@
module developer
module vcode
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.mcp.logger
@@ -97,9 +97,7 @@ fn get_type_from_module(module_path string, type_name string) !string {
// given a module path and a function name, returns the function definition of that function within that module
// for instance: get_function_from_module('lib/mcp/developer/vlang.v', 'develop') might return fn develop(...) {...}
fn get_function_from_module(module_path string, function_name string) !string {
println('Searching for function ${function_name} in module ${module_path}')
v_files := list_v_files(module_path) or {
println('Error listing files: ${err}')
return error('Failed to list V files in ${module_path}: ${err}')
}

View File

@@ -1,4 +1,4 @@
module developer
module vcode
import os

View File

@@ -0,0 +1,39 @@
module vcode
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.schemas.jsonschema
import x.json2 {Any}
const get_function_from_file_tool = mcp.Tool{
name: 'get_function_from_file'
description: 'get_function_from_file parses a V file and extracts a specific function block including its comments
ARGS:
file_path string - path to the V file
function_name string - name of the function to extract
RETURNS: string - the function block including comments, or empty string if not found'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {
'file_path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'function_name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['file_path', 'function_name']
}
}
pub fn (d &VCode) get_function_from_file_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
file_path := arguments['file_path'].str()
function_name := arguments['function_name'].str()
result := code.get_function_from_file(file_path, function_name) or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: mcp.result_to_mcp_tool_contents[string](result.vgen())
}
}

View File

@@ -0,0 +1,71 @@
module vcode
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.schemas.jsonschema
import x.json2 {Any}
const write_vfile_tool = mcp.Tool{
name: 'write_vfile'
description: 'write_vfile parses a V code string into a VFile and writes it to the specified path
ARGS:
path string - directory path where to write the file
code string - V code content to write
format bool - whether to format the code (optional, default: false)
overwrite bool - whether to overwrite existing file (optional, default: false)
prefix string - prefix to add to the filename (optional, default: "")
RETURNS: string - success message with the path of the written file'
input_schema: jsonschema.Schema{
typ: 'object'
properties: {
'path': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'code': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
'format': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'boolean'
})
'overwrite': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'boolean'
})
'prefix': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['path', 'code']
}
}
pub fn (d &VCode) write_vfile_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
path := arguments['path'].str()
code_str := arguments['code'].str()
// Parse optional parameters with defaults
format := if 'format' in arguments { arguments['format'].bool() } else { false }
overwrite := if 'overwrite' in arguments { arguments['overwrite'].bool() } else { false }
prefix := if 'prefix' in arguments { arguments['prefix'].str() } else { '' }
// Create write options
options := code.WriteOptions{
format: format
overwrite: overwrite
prefix: prefix
}
// Parse the V code string into a VFile
vfile := code.parse_vfile(code_str) or {
return mcp.error_tool_call_result(err)
}
// Write the VFile to the specified path
vfile.write(path, options) or {
return mcp.error_tool_call_result(err)
}
return mcp.ToolCallResult{
is_error: false
content: mcp.result_to_mcp_tool_contents[string]('Successfully wrote V file to ${path}')
}
}

View File

@@ -98,7 +98,13 @@ pub fn decode_response(data string) !Response {
// Returns:
// - A JSON string representation of the Response
pub fn (resp Response) encode() string {
return json2.encode(resp)
// Payload is already json string
if resp.error_ != none {
return '{"jsonrpc":"2.0","id":${resp.id},"error":${resp.error_.str()}}'
} else if resp.result != none {
return '{"jsonrpc":"2.0","id":${resp.id},"result":${resp.result}}'
}
return '{"jsonrpc":"2.0","id":${resp.id}}'
}
// validate checks that the Response object follows the JSON-RPC 2.0 specification.

View File

@@ -1,5 +1,6 @@
module codegen
import log
import freeflowuniverse.herolib.core.code { Alias, Array, Attribute, CodeItem, Object, Struct, StructField, Type, type_from_symbol }
import freeflowuniverse.herolib.schemas.jsonschema { Reference, Schema, SchemaRef }

131
manual/config.json Normal file
View File

@@ -0,0 +1,131 @@
{
"Sidebar": [
{
"Title": "General",
"Items": [
{
"Title": "Create Tag",
"Href": "/create_tag",
"IsDir": false
}
]
},
{
"Title": "Best Practices",
"Items": [
{
"Title": "Osal",
"Href": "/best_practices/osal",
"IsDir": true,
"Children": [
{
"Title": "Silence",
"Href": "/best_practices/osal/silence",
"IsDir": false
}
]
},
{
"Title": "Scripts",
"Href": "/best_practices/scripts",
"IsDir": true,
"Children": [
{
"Title": "Scripts",
"Href": "/best_practices/scripts/scripts",
"IsDir": false
},
{
"Title": "Shebang",
"Href": "/best_practices/scripts/shebang",
"IsDir": false
}
]
},
{
"Title": "Using Args In Function",
"Href": "/best_practices/using_args_in_function",
"IsDir": false
}
]
},
{
"Title": "Model Context Providers",
"Items": [
{
"Title": "Baobab MCP",
"Href": "/Users/timurgordon/code/github/freeflowuniverse/herolib/lib/mcp/baobab/README.md",
"IsDir": false
}
]
},
{
"Title": "Core",
"Items": [
{
"Title": "Base",
"Href": "/core/base",
"IsDir": false
},
{
"Title": "Concepts",
"Href": "/core/concepts",
"IsDir": true,
"Children": [
{
"Title": "Global Ids",
"Href": "/core/concepts/global_ids",
"IsDir": false
},
{
"Title": "Name Registry",
"Href": "/core/concepts/name_registry",
"IsDir": false
},
{
"Title": "Objects",
"Href": "/core/concepts/objects",
"IsDir": false
},
{
"Title": "Sid",
"Href": "/core/concepts/sid",
"IsDir": false
}
]
},
{
"Title": "Context",
"Href": "/core/context",
"IsDir": false
},
{
"Title": "Context Session Job",
"Href": "/core/context_session_job",
"IsDir": false
},
{
"Title": "Play",
"Href": "/core/play",
"IsDir": false
},
{
"Title": "Session",
"Href": "/core/session",
"IsDir": false
}
]
},
{
"Title": "Documentation",
"Items": [
{
"Title": "Docextractor",
"Href": "/documentation/docextractor",
"IsDir": false
}
]
}
],
"Title": "HeroLib Manual"
}

View File

@@ -8,290 +8,19 @@ echo "Starting HeroLib Manual Wiki Server..."
# Get the directory of this script (manual directory)
MANUAL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Get the directory of this script (manual directory)
CONFIG_FILE="$MANUAL_DIR/config.json"
# Path to the wiki package
WIKI_DIR="/Users/timurgordon/code/github/freeflowuniverse/herolauncher/pkg/ui/wiki"
# Path to the herolib directory
HEROLIB_DIR="/Users/timurgordon/code/github/freeflowuniverse/herolib"
# Check if the wiki directory exists
if [ ! -d "$WIKI_DIR" ]; then
echo "Error: Wiki directory not found at $WIKI_DIR"
exit 1
fi
# Check if the herolib directory exists
if [ ! -d "$HEROLIB_DIR" ]; then
echo "Error: HeroLib directory not found at $HEROLIB_DIR"
exit 1
fi
# Create a local VFS instance for the manual directory
echo "Creating local VFS for manual directory: $MANUAL_DIR"
cd "$HEROLIB_DIR"
# Create a temporary V program to initialize the VFS
TMP_DIR=$(mktemp -d)
VFS_INIT_FILE="$TMP_DIR/vfs_init.v"
cat > "$VFS_INIT_FILE" << 'EOL'
module main
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.vfs.vfs_local
import os
fn main() {
if os.args.len < 2 {
println('Usage: vfs_init <root_path>')
exit(1)
}
root_path := os.args[1]
println('Initializing local VFS with root path: ${root_path}')
vfs_impl := vfs_local.new_local_vfs(root_path) or {
println('Error creating local VFS: ${err}')
exit(1)
}
println('Local VFS initialized successfully')
}
EOL
# Compile and run the VFS initialization program
cd "$TMP_DIR"
v "$VFS_INIT_FILE"
"$TMP_DIR/vfs_init" "$MANUAL_DIR"
# Generate configuration JSON file with sidebar data
CONFIG_FILE="$TMP_DIR/wiki_config.json"
echo "Generating wiki configuration file: $CONFIG_FILE"
# Create a temporary Go program to generate the sidebar configuration
SIDEBAR_GEN_FILE="$TMP_DIR/sidebar_gen.go"
cat > "$SIDEBAR_GEN_FILE" << 'EOL'
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// SidebarItem represents an item in the sidebar
type SidebarItem struct {
Title string `json:"Title"`
Href string `json:"Href"`
IsDir bool `json:"IsDir"`
External bool `json:"External,omitempty"`
Children []SidebarItem `json:"Children,omitempty"`
}
// SidebarSection represents a section in the sidebar
type SidebarSection struct {
Title string `json:"Title"`
Items []SidebarItem `json:"Items"`
}
// Configuration represents the wiki configuration
type Configuration struct {
Sidebar []SidebarSection `json:"Sidebar"`
Title string `json:"Title,omitempty"`
BaseURL string `json:"BaseURL,omitempty"`
}
func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: sidebar_gen <content_path> <output_file>")
os.Exit(1)
}
contentPath := os.Args[1]
outputFile := os.Args[2]
// Generate sidebar data
sidebar, err := generateSidebarFromPath(contentPath)
if err != nil {
fmt.Printf("Error generating sidebar: %v\n", err)
os.Exit(1)
}
// Create configuration
config := Configuration{
Sidebar: sidebar,
Title: "HeroLib Manual",
}
// Write to file
configJSON, err := json.MarshalIndent(config, "", " ")
if err != nil {
fmt.Printf("Error marshaling JSON: %v\n", err)
os.Exit(1)
}
err = ioutil.WriteFile(outputFile, configJSON, 0644)
if err != nil {
fmt.Printf("Error writing file: %v\n", err)
os.Exit(1)
}
fmt.Printf("Configuration written to %s\n", outputFile)
}
// Generate sidebar data from content path
func generateSidebarFromPath(contentPath string) ([]SidebarSection, error) {
// Get absolute path for content directory
absContentPath, err := filepath.Abs(contentPath)
if err != nil {
return nil, fmt.Errorf("error getting absolute path: %w", err)
}
// Process top-level directories and files
dirs, err := ioutil.ReadDir(absContentPath)
if err != nil {
return nil, fmt.Errorf("error reading content directory: %w", err)
}
// Create sections for each top-level directory
var sections []SidebarSection
// Add files at the root level to a "General" section
var rootFiles []SidebarItem
// Process directories and files
for _, dir := range dirs {
if dir.IsDir() {
// Process directory
dirPath := filepath.Join(absContentPath, dir.Name())
// Pass the top-level directory name as the initial parent path
items, err := processDirectoryHierarchy(dirPath, absContentPath, dir.Name())
if err != nil {
return nil, fmt.Errorf("error processing directory %s: %w", dir.Name(), err)
}
if len(items) > 0 {
sections = append(sections, SidebarSection{
Title: formatTitle(dir.Name()),
Items: items,
})
}
} else if isMarkdownFile(dir.Name()) {
// Add root level markdown files to the General section
filePath := filepath.Join(absContentPath, dir.Name())
fileItem := createSidebarItemFromFile(filePath, absContentPath, "")
rootFiles = append(rootFiles, fileItem)
}
}
// Add root files to a General section if there are any
if len(rootFiles) > 0 {
sections = append([]SidebarSection{{
Title: "General",
Items: rootFiles,
}}, sections...)
}
return sections, nil
}
// Process a directory and return a hierarchical structure of sidebar items
func processDirectoryHierarchy(dirPath, rootPath, parentPath string) ([]SidebarItem, error) {
entries, err := ioutil.ReadDir(dirPath)
if err != nil {
return nil, fmt.Errorf("error reading directory %s: %w", dirPath, err)
}
var items []SidebarItem
// Process all entries in the directory
for _, entry := range entries {
entryPath := filepath.Join(dirPath, entry.Name())
relPath := filepath.Join(parentPath, entry.Name())
if entry.IsDir() {
// Process subdirectory
subItems, err := processDirectoryHierarchy(entryPath, rootPath, relPath)
if err != nil {
return nil, err
}
if len(subItems) > 0 {
// Create a directory item with children
items = append(items, SidebarItem{
Title: formatTitle(entry.Name()),
Href: "/" + relPath, // Add leading slash
IsDir: true,
Children: subItems,
})
}
} else if isMarkdownFile(entry.Name()) {
// Process markdown file
fileItem := createSidebarItemFromFile(entryPath, rootPath, parentPath)
items = append(items, fileItem)
}
}
return items, nil
}
// Create a sidebar item from a file path
func createSidebarItemFromFile(filePath, rootPath, parentPath string) SidebarItem {
fileName := filepath.Base(filePath)
baseName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
relPath := filepath.Join(parentPath, baseName)
return SidebarItem{
Title: formatTitle(baseName),
Href: "/" + relPath, // Add leading slash for proper URL formatting
IsDir: false,
}
}
// Format a title from a file or directory name
func formatTitle(name string) string {
// Replace underscores and hyphens with spaces
name = strings.ReplaceAll(name, "_", " ")
name = strings.ReplaceAll(name, "-", " ")
// Capitalize the first letter of each word
words := strings.Fields(name)
for i, word := range words {
if len(word) > 0 {
words[i] = strings.ToUpper(word[0:1]) + word[1:]
}
}
return strings.Join(words, " ")
}
// Check if a file is a markdown file
func isMarkdownFile(fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
return ext == ".md" || ext == ".markdown"
}
EOL
# Compile and run the sidebar generator
cd "$TMP_DIR"
go build -o sidebar_gen "$SIDEBAR_GEN_FILE"
"$TMP_DIR/sidebar_gen" "$MANUAL_DIR" "$CONFIG_FILE"
# Start the wiki server with the manual directory as the content path and config file
echo "Serving manual content from: $MANUAL_DIR"
echo "Using wiki server from: $WIKI_DIR"
cd "$WIKI_DIR"
# Display the generated configuration for debugging
echo "Generated configuration:"
cat "$CONFIG_FILE" | head -n 30
# Run the wiki server on port 3004
go run main.go "$MANUAL_DIR" "$CONFIG_FILE" 3004
go run . "$MANUAL_DIR" "$CONFIG_FILE" 3004
# The script will not reach this point unless the server is stopped
echo "Wiki server stopped."