diff --git a/lib/mcp/v_do/README.md b/lib/mcp/developer/README.md similarity index 100% rename from lib/mcp/v_do/README.md rename to lib/mcp/developer/README.md diff --git a/lib/mcp/developer/developer.v b/lib/mcp/developer/developer.v new file mode 100644 index 00000000..638cf3f9 --- /dev/null +++ b/lib/mcp/developer/developer.v @@ -0,0 +1,4 @@ +module developer + +pub struct Developer {} + diff --git a/lib/mcp/developer/generate_mcp.v b/lib/mcp/developer/generate_mcp.v new file mode 100644 index 00000000..e18c008a --- /dev/null +++ b/lib/mcp/developer/generate_mcp.v @@ -0,0 +1,304 @@ +module developer + +import freeflowuniverse.herolib.mcp + + +// 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 +} \ No newline at end of file diff --git a/lib/mcp/developer/generate_mcp_test.v b/lib/mcp/developer/generate_mcp_test.v new file mode 100644 index 00000000..f27fbe9c --- /dev/null +++ b/lib/mcp/developer/generate_mcp_test.v @@ -0,0 +1,175 @@ +module developer + +import freeflowuniverse.herolib.mcp +import json + +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\'s 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') +} + +// Run all tests +fn main() { + test_parse_struct_fields() + test_create_mcp_tool_input_schema() + test_create_mcp_tool() + + println('All tests passed successfully!') +} diff --git a/lib/mcp/developer/generate_mcp_tools.v b/lib/mcp/developer/generate_mcp_tools.v new file mode 100644 index 00000000..ce945c1d --- /dev/null +++ b/lib/mcp/developer/generate_mcp_tools.v @@ -0,0 +1,21 @@ +module developer + +import freeflowuniverse.herolib.mcp + +// 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'] + } +} diff --git a/lib/mcp/developer/mcp.v b/lib/mcp/developer/mcp.v new file mode 100644 index 00000000..e69de29b diff --git a/lib/mcp/developer/templates/prompt_generate_mcp.txt b/lib/mcp/developer/templates/prompt_generate_mcp.txt new file mode 100644 index 00000000..e69de29b diff --git a/lib/mcp/developer/vlang.v b/lib/mcp/developer/vlang.v new file mode 100644 index 00000000..c65bdb49 --- /dev/null +++ b/lib/mcp/developer/vlang.v @@ -0,0 +1,88 @@ +module developer + +import freeflowuniverse.herolib.mcp +import os + +// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v +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 +} + +// test runs v test on the specified file or directory +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}' + } +} + + +// vvet runs v vet on the specified file or directory +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) + } +} + +// vet_file runs v vet on a single file +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}' +} + +// cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath}' \ No newline at end of file diff --git a/lib/mcp/v_do/handlers/vcompile.v b/lib/mcp/v_do/handlers/vcompile.v deleted file mode 100644 index dd91c85b..00000000 --- a/lib/mcp/v_do/handlers/vcompile.v +++ /dev/null @@ -1,4 +0,0 @@ -module handlers - -import os -import freeflowuniverse.herolib.mcp.v_do.logger diff --git a/lib/mcp/v_do/handlers/vlist.v b/lib/mcp/v_do/handlers/vlist.v deleted file mode 100644 index f3e3ab71..00000000 --- a/lib/mcp/v_do/handlers/vlist.v +++ /dev/null @@ -1,21 +0,0 @@ -module handlers - -import os -import freeflowuniverse.herolib.mcp.v_do.logger - -// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v -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 -} diff --git a/lib/mcp/v_do/handlers/vrun.v b/lib/mcp/v_do/handlers/vrun.v deleted file mode 100644 index 1d327cbf..00000000 --- a/lib/mcp/v_do/handlers/vrun.v +++ /dev/null @@ -1,8 +0,0 @@ -module handlers - -import os -import freeflowuniverse.herolib.mcp.v_do.logger - - - -// cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath}' \ No newline at end of file diff --git a/lib/mcp/v_do/handlers/vtest.v b/lib/mcp/v_do/handlers/vtest.v deleted file mode 100644 index b10ad59a..00000000 --- a/lib/mcp/v_do/handlers/vtest.v +++ /dev/null @@ -1,31 +0,0 @@ -module handlers - -import os -import freeflowuniverse.herolib.mcp.v_do.logger - -// test runs v test on the specified file or directory -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}' - } - -} diff --git a/lib/mcp/v_do/handlers/vvet.v b/lib/mcp/v_do/handlers/vvet.v deleted file mode 100644 index a50afb75..00000000 --- a/lib/mcp/v_do/handlers/vvet.v +++ /dev/null @@ -1,42 +0,0 @@ -module handlers - -import os -import freeflowuniverse.herolib.mcp.v_do.logger - -// vvet runs v vet on the specified file or directory -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) - } -} - -// vet_file runs v vet on a single file -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}' -} diff --git a/lib/mcp/v_do/vdo b/lib/mcp/v_do/vdo index b2110d26..ec83b3cc 100755 Binary files a/lib/mcp/v_do/vdo and b/lib/mcp/v_do/vdo differ diff --git a/lib/mcp/v_do/vdo.v b/lib/mcp/v_do/vdo.v index fc1f99b9..d0e63a4f 100644 --- a/lib/mcp/v_do/vdo.v +++ b/lib/mcp/v_do/vdo.v @@ -12,15 +12,17 @@ fn main() { // Initialize the server with the empty handlers map mut server := mcp.new_server( - handlers, - mcp.ServerConfiguration{ + mcp.MemoryBackend{}, + mcp.ServerParams{ + handlers: handlers, + config:mcp.ServerConfiguration{ server_info: mcp.ServerInfo{ name: 'v_do' version: '1.0.0' } - } + }} )! - + server.start() or { logger.fatal('Error starting server: $err') exit(1)