From bf26b0af1dba755022d9af9e18467889291481fb Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Wed, 26 Mar 2025 19:31:07 +0100 Subject: [PATCH 1/8] manual updates --- manual/config.json | 131 ++++++++++++++++++++ manual/serve_wiki.sh | 279 +------------------------------------------ 2 files changed, 135 insertions(+), 275 deletions(-) create mode 100644 manual/config.json diff --git a/manual/config.json b/manual/config.json new file mode 100644 index 00000000..2e1d606d --- /dev/null +++ b/manual/config.json @@ -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" + } \ No newline at end of file diff --git a/manual/serve_wiki.sh b/manual/serve_wiki.sh index 0396147f..c4c70de3 100755 --- a/manual/serve_wiki.sh +++ b/manual/serve_wiki.sh @@ -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 ') - 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 ") - 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." From 186c3aae59f8b36f671d1560fba589a8bdb0ab56 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:20:56 +0100 Subject: [PATCH 2/8] reorganize and sort mcps --- TOSORT/developer/developer.v | 61 --- TOSORT/developer/generate_mcp.v | 391 ------------------ TOSORT/developer/generate_mcp_test.v | 205 --------- TOSORT/developer/generate_mcp_tools.v | 108 ----- TOSORT/developer/mcp.v | 31 -- TOSORT/developer/scripts/run.vsh | 10 - .../templates/prompt_generate_mcp.txt | 0 .../templates/tool_handler.v.template | 11 - TOSORT/developer/testdata/mock_module/mock.v | 38 -- .../testdata/vlang_test_standalone.v | 159 ------- TOSORT/developer/vlang.v | 286 ------------- lib/mcp/README.md | 62 +-- lib/mcp/backend_memory.v | 3 +- lib/mcp/baobab/README.md | 3 + lib/mcp/baobab/baobab_tools.v | 111 +++-- lib/mcp/baobab/baobab_tools_test.v | 101 +++++ lib/mcp/baobab/command.v | 23 ++ lib/mcp/baobab/developer.v | 57 --- lib/mcp/baobab/mcp_test.v | 128 ++++++ lib/mcp/baobab/server.v | 34 ++ lib/mcp/error.v | 11 - lib/mcp/factory.v | 58 +-- lib/mcp/generics.v | 52 +++ lib/mcp/handler_tools.v | 34 +- lib/mcp/mcpgen/README.md | 92 +++++ lib/mcp/mcpgen/command.v | 23 ++ lib/mcp/mcpgen/mcpgen.v | 283 +++++++++++++ .../mcp/mcpgen/mcpgen_helpers.v | 3 +- lib/mcp/mcpgen/mcpgen_tools.v | 145 +++++++ .../create_mcp_tools_code_tool_input.json | 21 + lib/mcp/mcpgen/server.v | 35 ++ .../mcpgen}/templates/tool_code.v.template | 1 + .../mcpgen/templates/tool_handler.v.template | 11 + .../mcpgen/templates/tools_file.v.template | 1 + lib/mcp/tool_handler.v | 6 - lib/mcp/{v_do => vcode}/.gitignore | 0 {TOSORT/developer => lib/mcp/vcode}/README.md | 0 lib/mcp/vcode/command.v | 15 + lib/mcp/{v_do => vcode}/new.json | 0 lib/mcp/{baobab/mcp.v => vcode/server.v} | 18 +- lib/mcp/{v_do => vcode}/test_client.vsh | 0 lib/mcp/{v_do => vcode}/vdo.v | 2 +- .../developer => lib/mcp/vcode}/vlang_test.v | 2 +- .../developer => lib/mcp/vcode}/vlang_tools.v | 8 +- 44 files changed, 1163 insertions(+), 1480 deletions(-) delete mode 100644 TOSORT/developer/developer.v delete mode 100644 TOSORT/developer/generate_mcp.v delete mode 100644 TOSORT/developer/generate_mcp_test.v delete mode 100644 TOSORT/developer/generate_mcp_tools.v delete mode 100644 TOSORT/developer/mcp.v delete mode 100755 TOSORT/developer/scripts/run.vsh delete mode 100644 TOSORT/developer/templates/prompt_generate_mcp.txt delete mode 100644 TOSORT/developer/templates/tool_handler.v.template delete mode 100644 TOSORT/developer/testdata/mock_module/mock.v delete mode 100644 TOSORT/developer/testdata/vlang_test_standalone.v delete mode 100644 TOSORT/developer/vlang.v create mode 100644 lib/mcp/baobab/README.md create mode 100644 lib/mcp/baobab/baobab_tools_test.v create mode 100644 lib/mcp/baobab/command.v delete mode 100644 lib/mcp/baobab/developer.v create mode 100644 lib/mcp/baobab/mcp_test.v create mode 100644 lib/mcp/baobab/server.v delete mode 100644 lib/mcp/error.v create mode 100644 lib/mcp/mcpgen/README.md create mode 100644 lib/mcp/mcpgen/command.v create mode 100644 lib/mcp/mcpgen/mcpgen.v rename TOSORT/developer/generate_mcp_helpers.v => lib/mcp/mcpgen/mcpgen_helpers.v (97%) create mode 100644 lib/mcp/mcpgen/mcpgen_tools.v create mode 100644 lib/mcp/mcpgen/schemas/create_mcp_tools_code_tool_input.json create mode 100644 lib/mcp/mcpgen/server.v rename {TOSORT/developer => lib/mcp/mcpgen}/templates/tool_code.v.template (67%) create mode 100644 lib/mcp/mcpgen/templates/tool_handler.v.template create mode 100644 lib/mcp/mcpgen/templates/tools_file.v.template delete mode 100644 lib/mcp/tool_handler.v rename lib/mcp/{v_do => vcode}/.gitignore (100%) rename {TOSORT/developer => lib/mcp/vcode}/README.md (100%) create mode 100644 lib/mcp/vcode/command.v rename lib/mcp/{v_do => vcode}/new.json (100%) rename lib/mcp/{baobab/mcp.v => vcode/server.v} (60%) rename lib/mcp/{v_do => vcode}/test_client.vsh (100%) rename lib/mcp/{v_do => vcode}/vdo.v (98%) rename {TOSORT/developer => lib/mcp/vcode}/vlang_test.v (99%) rename {TOSORT/developer => lib/mcp/vcode}/vlang_tools.v (84%) diff --git a/TOSORT/developer/developer.v b/TOSORT/developer/developer.v deleted file mode 100644 index ff1119b6..00000000 --- a/TOSORT/developer/developer.v +++ /dev/null @@ -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 -} diff --git a/TOSORT/developer/generate_mcp.v b/TOSORT/developer/generate_mcp.v deleted file mode 100644 index a02b6e27..00000000 --- a/TOSORT/developer/generate_mcp.v +++ /dev/null @@ -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 -} diff --git a/TOSORT/developer/generate_mcp_test.v b/TOSORT/developer/generate_mcp_test.v deleted file mode 100644 index d386ac4a..00000000 --- a/TOSORT/developer/generate_mcp_test.v +++ /dev/null @@ -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') -// } diff --git a/TOSORT/developer/generate_mcp_tools.v b/TOSORT/developer/generate_mcp_tools.v deleted file mode 100644 index 9195e3c9..00000000 --- a/TOSORT/developer/generate_mcp_tools.v +++ /dev/null @@ -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) - } -} diff --git a/TOSORT/developer/mcp.v b/TOSORT/developer/mcp.v deleted file mode 100644 index f5ee4d97..00000000 --- a/TOSORT/developer/mcp.v +++ /dev/null @@ -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 -// } diff --git a/TOSORT/developer/scripts/run.vsh b/TOSORT/developer/scripts/run.vsh deleted file mode 100755 index 7f7dc891..00000000 --- a/TOSORT/developer/scripts/run.vsh +++ /dev/null @@ -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) -} diff --git a/TOSORT/developer/templates/prompt_generate_mcp.txt b/TOSORT/developer/templates/prompt_generate_mcp.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/TOSORT/developer/templates/tool_handler.v.template b/TOSORT/developer/templates/tool_handler.v.template deleted file mode 100644 index f2b1fe93..00000000 --- a/TOSORT/developer/templates/tool_handler.v.template +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/TOSORT/developer/testdata/mock_module/mock.v b/TOSORT/developer/testdata/mock_module/mock.v deleted file mode 100644 index 4ac7b86d..00000000 --- a/TOSORT/developer/testdata/mock_module/mock.v +++ /dev/null @@ -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}' -} diff --git a/TOSORT/developer/testdata/vlang_test_standalone.v b/TOSORT/developer/testdata/vlang_test_standalone.v deleted file mode 100644 index ef1fd6c6..00000000 --- a/TOSORT/developer/testdata/vlang_test_standalone.v +++ /dev/null @@ -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() -} diff --git a/TOSORT/developer/vlang.v b/TOSORT/developer/vlang.v deleted file mode 100644 index 936f5a6e..00000000 --- a/TOSORT/developer/vlang.v +++ /dev/null @@ -1,286 +0,0 @@ -module developer - -import freeflowuniverse.herolib.mcp -import freeflowuniverse.herolib.mcp.logger -import os -import log - -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('/')}' -} - -// given a module path and a type name, returns the type definition of that type within that module -// for instance: get_type_from_module('lib/mcp/developer/vlang.v', 'Developer') might return struct Developer {...} -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}') -} - -// 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}') - } - - println('Found ${v_files.len} V files in ${module_path}') - for v_file in v_files { - println('Checking file: ${v_file}') - result := get_function_from_file(v_file, function_name) or { - println('Function not found in ${v_file}: ${err}') - continue - } - println('Found function ${function_name} in ${v_file}') - return result - } - - return error('function ${function_name} not found in module ${module_path}') -} - -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 empty string if not found -fn get_function_from_file(file_path string, function_name string) !string { - content := os.read_file(file_path) or { - return error('Failed to read file: ${file_path}: ${err}') - } - - lines := content.split_into_lines() - mut result := []string{} - mut in_function := false - mut brace_count := 0 - mut comment_block := []string{} - - for i, line in lines { - trimmed := line.trim_space() - - // Collect comments that might be above the function - if trimmed.starts_with('//') { - if !in_function { - comment_block << line - } else if brace_count > 0 { - result << line - } - continue - } - - // Check if we found the function - if !in_function && (trimmed.starts_with('fn ${function_name}(') - || trimmed.starts_with('pub fn ${function_name}(')) { - in_function = true - // Add collected comments - result << comment_block - comment_block = [] - result << line - if line.contains('{') { - brace_count++ - } - continue - } - - // If we're inside the function, keep track of braces - if in_function { - result << line - - for c in line { - if c == `{` { - brace_count++ - } else if c == `}` { - brace_count-- - } - } - - // If brace_count is 0, we've reached the end of the function - if brace_count == 0 && trimmed.contains('}') { - return result.join('\n') - } - } else { - // Reset comment block if we pass a blank line - if trimmed == '' { - comment_block = [] - } - } - } - - if !in_function { - return error('Function "${function_name}" not found in ${file_path}') - } - - return result.join('\n') -} - -// 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}' diff --git a/lib/mcp/README.md b/lib/mcp/README.md index 707ca45c..74f1a5cb 100644 --- a/lib/mcp/README.md +++ b/lib/mcp/README.md @@ -4,54 +4,70 @@ 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()! ``` -## Development Tools +## Sub-modules -The module includes several development tools accessible through the `v_do` directory: +The MCP directory contains several sub-modules that implement specific MCP servers: -- **test**: Run tests for V files -- **run**: Execute V files -- **compile**: Compile V files -- **vet**: Perform static analysis on V files +- **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 diff --git a/lib/mcp/backend_memory.v b/lib/mcp/backend_memory.v index 61d47da5..3a83d59f 100644 --- a/lib/mcp/backend_memory.v +++ b/lib/mcp/backend_memory.v @@ -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()}' }, ] } diff --git a/lib/mcp/baobab/README.md b/lib/mcp/baobab/README.md new file mode 100644 index 00000000..64fa9914 --- /dev/null +++ b/lib/mcp/baobab/README.md @@ -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. \ No newline at end of file diff --git a/lib/mcp/baobab/baobab_tools.v b/lib/mcp/baobab/baobab_tools.v index 9497eb4e..7866c6f3 100644 --- a/lib/mcp/baobab/baobab_tools.v +++ b/lib/mcp/baobab/baobab_tools.v @@ -1,40 +1,101 @@ 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 -const generate_module_from_openapi_tool = mcp.Tool{ - name: 'generate_module_from_openapi' - description: '' - input_schema: mcp.ToolInputSchema{ - typ: 'object' - properties: { - 'openapi_path': mcp.ToolProperty{ - typ: 'string' - items: mcp.ToolItems{ - typ: '' - enum: [] - } - enum: [] +// 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: ['openapi_path'] - } + })} + required: ['source'] + } } -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_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) } - 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_module_from_openapi MCP Tool +const generate_module_from_openapi_tool = mcp.Tool{ + name: 'generate_module_from_openapi' + description: '' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: {'openapi_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + })} + required: ['openapi_path'] + } +} + +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) + } + return mcp.ToolCallResult{ + is_error: false + 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) + } +} \ No newline at end of file diff --git a/lib/mcp/baobab/baobab_tools_test.v b/lib/mcp/baobab/baobab_tools_test.v new file mode 100644 index 00000000..3101e129 --- /dev/null +++ b/lib/mcp/baobab/baobab_tools_test.v @@ -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' + } +} \ No newline at end of file diff --git a/lib/mcp/baobab/command.v b/lib/mcp/baobab/command.v new file mode 100644 index 00000000..7fe612ba --- /dev/null +++ b/lib/mcp/baobab/command.v @@ -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()! +} \ No newline at end of file diff --git a/lib/mcp/baobab/developer.v b/lib/mcp/baobab/developer.v deleted file mode 100644 index db77935a..00000000 --- a/lib/mcp/baobab/developer.v +++ /dev/null @@ -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 -} diff --git a/lib/mcp/baobab/mcp_test.v b/lib/mcp/baobab/mcp_test.v new file mode 100644 index 00000000..0d33df6a --- /dev/null +++ b/lib/mcp/baobab/mcp_test.v @@ -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' +} diff --git a/lib/mcp/baobab/server.v b/lib/mcp/baobab/server.v new file mode 100644 index 00000000..d309288e --- /dev/null +++ b/lib/mcp/baobab/server.v @@ -0,0 +1,34 @@ +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 + } + 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 + } + }, mcp.ServerParams{ + config: mcp.ServerConfiguration{ + server_info: mcp.ServerInfo{ + name: 'baobab' + version: '1.0.0' + } + } + })! + return server +} \ No newline at end of file diff --git a/lib/mcp/error.v b/lib/mcp/error.v deleted file mode 100644 index 3e9c4aea..00000000 --- a/lib/mcp/error.v +++ /dev/null @@ -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() - }] - } -} diff --git a/lib/mcp/factory.v b/lib/mcp/factory.v index 3b88899c..fe4cbd9a 100644 --- a/lib/mcp/factory.v +++ b/lib/mcp/factory.v @@ -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 +} diff --git a/lib/mcp/generics.v b/lib/mcp/generics.v index 2abe85a2..4dde2f1b 100644 --- a/lib/mcp/generics.v +++ b/lib/mcp/generics.v @@ -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 +} \ No newline at end of file diff --git a/lib/mcp/handler_tools.v b/lib/mcp/handler_tools.v index c10fa7f0..f597f069 100644 --- a/lib/mcp/handler_tools.v +++ b/lib/mcp/handler_tools.v @@ -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() + }] + } +} \ No newline at end of file diff --git a/lib/mcp/mcpgen/README.md b/lib/mcp/mcpgen/README.md new file mode 100644 index 00000000..550ecfed --- /dev/null +++ b/lib/mcp/mcpgen/README.md @@ -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: + +{"jsonrpc":"2.0","id":"","method":"","params":{"fullpath":""}} +``` + +Where: +- `` is the length of the JSON message in bytes +- `` is a unique identifier for the request +- `` is one of: `test`, `run`, `compile`, or `vet` +- `` 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} +``` diff --git a/lib/mcp/mcpgen/command.v b/lib/mcp/mcpgen/command.v new file mode 100644 index 00000000..37735734 --- /dev/null +++ b/lib/mcp/mcpgen/command.v @@ -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()! +} \ No newline at end of file diff --git a/lib/mcp/mcpgen/mcpgen.v b/lib/mcp/mcpgen/mcpgen.v new file mode 100644 index 00000000..53539ba8 --- /dev/null +++ b/lib/mcp/mcpgen/mcpgen.v @@ -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 +} diff --git a/TOSORT/developer/generate_mcp_helpers.v b/lib/mcp/mcpgen/mcpgen_helpers.v similarity index 97% rename from TOSORT/developer/generate_mcp_helpers.v rename to lib/mcp/mcpgen/mcpgen_helpers.v index 2600a971..c95b934c 100644 --- a/TOSORT/developer/generate_mcp_helpers.v +++ b/lib/mcp/mcpgen/mcpgen_helpers.v @@ -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)] diff --git a/lib/mcp/mcpgen/mcpgen_tools.v b/lib/mcp/mcpgen/mcpgen_tools.v new file mode 100644 index 00000000..f583502c --- /dev/null +++ b/lib/mcp/mcpgen/mcpgen_tools.v @@ -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) + } +} diff --git a/lib/mcp/mcpgen/schemas/create_mcp_tools_code_tool_input.json b/lib/mcp/mcpgen/schemas/create_mcp_tools_code_tool_input.json new file mode 100644 index 00000000..ad39a778 --- /dev/null +++ b/lib/mcp/mcpgen/schemas/create_mcp_tools_code_tool_input.json @@ -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"] + } \ No newline at end of file diff --git a/lib/mcp/mcpgen/server.v b/lib/mcp/mcpgen/server.v new file mode 100644 index 00000000..21bd0d1f --- /dev/null +++ b/lib/mcp/mcpgen/server.v @@ -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 +} \ No newline at end of file diff --git a/TOSORT/developer/templates/tool_code.v.template b/lib/mcp/mcpgen/templates/tool_code.v.template similarity index 67% rename from TOSORT/developer/templates/tool_code.v.template rename to lib/mcp/mcpgen/templates/tool_code.v.template index b4ad19b5..99a69fdd 100644 --- a/TOSORT/developer/templates/tool_code.v.template +++ b/lib/mcp/mcpgen/templates/tool_code.v.template @@ -1,4 +1,5 @@ // @{tool_name} MCP Tool +// @{tool.description} const @{tool_name}_tool = @{tool.str()} diff --git a/lib/mcp/mcpgen/templates/tool_handler.v.template b/lib/mcp/mcpgen/templates/tool_handler.v.template new file mode 100644 index 00000000..1f7fa119 --- /dev/null +++ b/lib/mcp/mcpgen/templates/tool_handler.v.template @@ -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) + } +} \ No newline at end of file diff --git a/lib/mcp/mcpgen/templates/tools_file.v.template b/lib/mcp/mcpgen/templates/tools_file.v.template new file mode 100644 index 00000000..b6d14b0b --- /dev/null +++ b/lib/mcp/mcpgen/templates/tools_file.v.template @@ -0,0 +1 @@ +@for import in \ No newline at end of file diff --git a/lib/mcp/tool_handler.v b/lib/mcp/tool_handler.v deleted file mode 100644 index feccaf16..00000000 --- a/lib/mcp/tool_handler.v +++ /dev/null @@ -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 diff --git a/lib/mcp/v_do/.gitignore b/lib/mcp/vcode/.gitignore similarity index 100% rename from lib/mcp/v_do/.gitignore rename to lib/mcp/vcode/.gitignore diff --git a/TOSORT/developer/README.md b/lib/mcp/vcode/README.md similarity index 100% rename from TOSORT/developer/README.md rename to lib/mcp/vcode/README.md diff --git a/lib/mcp/vcode/command.v b/lib/mcp/vcode/command.v new file mode 100644 index 00000000..2780b6a6 --- /dev/null +++ b/lib/mcp/vcode/command.v @@ -0,0 +1,15 @@ +module vcode + +import cli + +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()! +} diff --git a/lib/mcp/v_do/new.json b/lib/mcp/vcode/new.json similarity index 100% rename from lib/mcp/v_do/new.json rename to lib/mcp/vcode/new.json diff --git a/lib/mcp/baobab/mcp.v b/lib/mcp/vcode/server.v similarity index 60% rename from lib/mcp/baobab/mcp.v rename to lib/mcp/vcode/server.v index 9c400607..64a0f5a0 100644 --- a/lib/mcp/baobab/mcp.v +++ b/lib/mcp/vcode/server.v @@ -1,27 +1,31 @@ -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 } tool_handlers: { - 'generate_module_from_openapi': generate_module_from_openapi_tool_handler + 'get_function_from_file': v.get_function_from_file_tool_handler } }, mcp.ServerParams{ config: mcp.ServerConfiguration{ server_info: mcp.ServerInfo{ - name: 'developer' + name: 'vcode' version: '1.0.0' } } })! return server -} +} \ No newline at end of file diff --git a/lib/mcp/v_do/test_client.vsh b/lib/mcp/vcode/test_client.vsh similarity index 100% rename from lib/mcp/v_do/test_client.vsh rename to lib/mcp/vcode/test_client.vsh diff --git a/lib/mcp/v_do/vdo.v b/lib/mcp/vcode/vdo.v similarity index 98% rename from lib/mcp/v_do/vdo.v rename to lib/mcp/vcode/vdo.v index 0a6e3578..534af27c 100644 --- a/lib/mcp/v_do/vdo.v +++ b/lib/mcp/vcode/vdo.v @@ -1,4 +1,4 @@ -module main +module vcode import freeflowuniverse.herolib.mcp.logger import freeflowuniverse.herolib.mcp diff --git a/TOSORT/developer/vlang_test.v b/lib/mcp/vcode/vlang_test.v similarity index 99% rename from TOSORT/developer/vlang_test.v rename to lib/mcp/vcode/vlang_test.v index 3ec33054..ade88519 100644 --- a/TOSORT/developer/vlang_test.v +++ b/lib/mcp/vcode/vlang_test.v @@ -1,4 +1,4 @@ -module developer +module vcode import os diff --git a/TOSORT/developer/vlang_tools.v b/lib/mcp/vcode/vlang_tools.v similarity index 84% rename from TOSORT/developer/vlang_tools.v rename to lib/mcp/vcode/vlang_tools.v index 95282cde..f53b5cd8 100644 --- a/TOSORT/developer/vlang_tools.v +++ b/lib/mcp/vcode/vlang_tools.v @@ -1,4 +1,4 @@ -module developer +module vcode import freeflowuniverse.herolib.mcp @@ -9,10 +9,10 @@ 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{ + input_schema: jsonschema.Schema{ typ: 'object' properties: { - 'file_path': mcp.ToolProperty{ + 'file_path': jsonschema.Schema{ typ: 'string' items: mcp.ToolItems{ typ: '' @@ -20,7 +20,7 @@ RETURNS: string - the function block including comments, or empty string if not } enum: [] } - 'function_name': mcp.ToolProperty{ + 'function_name': jsonschema.Schema{ typ: 'string' items: mcp.ToolItems{ typ: '' From 712b46864a9969e33d39fa3c34241deb753bd439 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:21:55 +0100 Subject: [PATCH 3/8] implement more code functionalities --- lib/core/code/README.md | 37 +++- lib/core/code/model_file.v | 120 ++++++++++- lib/core/code/model_file_test.v | 87 ++++++++ lib/core/code/model_module.v | 8 - lib/core/code/vlang_utils.v | 345 ++++++++++++++++++++++++++++++++ lib/mcp/vcode/vlang.v | 284 ++++++++++++++++++++++++++ 6 files changed, 871 insertions(+), 10 deletions(-) create mode 100644 lib/core/code/model_file_test.v create mode 100644 lib/core/code/vlang_utils.v create mode 100644 lib/mcp/vcode/vlang.v diff --git a/lib/core/code/README.md b/lib/core/code/README.md index 6bb1c215..cee3334d 100644 --- a/lib/core/code/README.md +++ b/lib/core/code/README.md @@ -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. \ No newline at end of file +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. diff --git a/lib/core/code/model_file.v b/lib/core/code/model_file.v index 64235922..28ed8da5 100644 --- a/lib/core/code/model_file.v +++ b/lib/core/code/model_file.v @@ -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 +} diff --git a/lib/core/code/model_file_test.v b/lib/core/code/model_file_test.v new file mode 100644 index 00000000..ca421f4d --- /dev/null +++ b/lib/core/code/model_file_test.v @@ -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' +} diff --git a/lib/core/code/model_module.v b/lib/core/code/model_module.v index a920fed7..fcdc839d 100644 --- a/lib/core/code/model_module.v +++ b/lib/core/code/model_module.v @@ -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()! } diff --git a/lib/core/code/vlang_utils.v b/lib/core/code/vlang_utils.v new file mode 100644 index 00000000..a3e715ff --- /dev/null +++ b/lib/core/code/vlang_utils.v @@ -0,0 +1,345 @@ +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) !string { +// log.error('Looking for function ${function_name} in file ${file_path}') +// 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 V file: ${file_path}: ${err}') +// } + +// for fn in vfile.functions() { +// if fn.name == function_name { +// return fn.code +// } +// } + +// return error('Function ${function_name} not found in ${file_path}') +// } + +// lines := content.split_into_lines() +// mut result := []string{} +// mut in_function := false +// mut brace_count := 0 +// mut comment_block := []string{} + +// for i, line in lines { +// trimmed := line.trim_space() + +// // Collect comments that might be above the function +// if trimmed.starts_with('//') { +// if !in_function { +// comment_block << line +// } else if brace_count > 0 { +// result << line +// } +// continue +// } + +// // Check if we found the function +// if !in_function && (trimmed.starts_with('fn ${function_name}(') +// || trimmed.starts_with('pub fn ${function_name}(')) { +// in_function = true +// // Add collected comments +// result << comment_block +// comment_block = [] +// result << line +// if line.contains('{') { +// brace_count++ +// } +// continue +// } + +// // If we're inside the function, keep track of braces +// if in_function { +// result << line + +// for c in line { +// if c == `{` { +// brace_count++ +// } else if c == `}` { +// brace_count-- +// } +// } + +// // If brace_count is 0, we've reached the end of the function +// if brace_count == 0 && trimmed.contains('}') { +// return result.join('\n') +// } +// } else { +// // Reset comment block if we pass a blank line +// if trimmed == '' { +// comment_block = [] +// } +// } +// } + +// if !in_function { +// return error('Function "${function_name}" not found in ${file_path}') +// } + +// return result.join('\n') +// } + +// 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) + } +} diff --git a/lib/mcp/vcode/vlang.v b/lib/mcp/vcode/vlang.v new file mode 100644 index 00000000..0ac89da1 --- /dev/null +++ b/lib/mcp/vcode/vlang.v @@ -0,0 +1,284 @@ +module vcode + +import freeflowuniverse.herolib.mcp +import freeflowuniverse.herolib.mcp.logger +import os +import log + +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('/')}' +} + +// given a module path and a type name, returns the type definition of that type within that module +// for instance: get_type_from_module('lib/mcp/developer/vlang.v', 'Developer') might return struct Developer {...} +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}') +} + +// 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 { + v_files := list_v_files(module_path) or { + return error('Failed to list V files in ${module_path}: ${err}') + } + + println('Found ${v_files.len} V files in ${module_path}') + for v_file in v_files { + println('Checking file: ${v_file}') + result := get_function_from_file(v_file, function_name) or { + println('Function not found in ${v_file}: ${err}') + continue + } + println('Found function ${function_name} in ${v_file}') + return result + } + + return error('function ${function_name} not found in module ${module_path}') +} + +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 empty string if not found +fn get_function_from_file(file_path string, function_name string) !string { + content := os.read_file(file_path) or { + return error('Failed to read file: ${file_path}: ${err}') + } + + lines := content.split_into_lines() + mut result := []string{} + mut in_function := false + mut brace_count := 0 + mut comment_block := []string{} + + for i, line in lines { + trimmed := line.trim_space() + + // Collect comments that might be above the function + if trimmed.starts_with('//') { + if !in_function { + comment_block << line + } else if brace_count > 0 { + result << line + } + continue + } + + // Check if we found the function + if !in_function && (trimmed.starts_with('fn ${function_name}(') + || trimmed.starts_with('pub fn ${function_name}(')) { + in_function = true + // Add collected comments + result << comment_block + comment_block = [] + result << line + if line.contains('{') { + brace_count++ + } + continue + } + + // If we're inside the function, keep track of braces + if in_function { + result << line + + for c in line { + if c == `{` { + brace_count++ + } else if c == `}` { + brace_count-- + } + } + + // If brace_count is 0, we've reached the end of the function + if brace_count == 0 && trimmed.contains('}') { + return result.join('\n') + } + } else { + // Reset comment block if we pass a blank line + if trimmed == '' { + comment_block = [] + } + } + } + + if !in_function { + return error('Function "${function_name}" not found in ${file_path}') + } + + return result.join('\n') +} + +// 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}' From 673d982360273f54fe06947cdd1802e882e0cda5 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:22:19 +0100 Subject: [PATCH 4/8] add baobab methods for easier mcp integration --- lib/baobab/generator/generate_methods.v | 25 ++++++++++++++++--- .../generator/generate_methods_interface.v | 12 +++++++++ lib/baobab/specification/from_openapi.v | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/baobab/generator/generate_methods.v b/lib/baobab/generator/generate_methods.v index 2c603c3f..5f212381 100644 --- a/lib/baobab/generator/generate_methods.v +++ b/lib/baobab/generator/generate_methods.v @@ -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 source.openapi_path != "" { + specification.from_openapi(openapi.new(path: source.openapi_path)!)! + } else if source.openrpc_path != "" { + specification.from_openrpc(openrpc.new(path: source.openrpc_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) @@ -136,7 +153,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' } diff --git a/lib/baobab/generator/generate_methods_interface.v b/lib/baobab/generator/generate_methods_interface.v index a9b27b6d..7492fe65 100644 --- a/lib/baobab/generator/generate_methods_interface.v +++ b/lib/baobab/generator/generate_methods_interface.v @@ -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 source.openapi_path != "" { + specification.from_openapi(openapi.new(path: source.openapi_path)!)! + } else if source.openrpc_path != "" { + specification.from_openrpc(openrpc.new(path: source.openrpc_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{ diff --git a/lib/baobab/specification/from_openapi.v b/lib/baobab/specification/from_openapi.v index 6b67f8b5..a28c7ed4 100644 --- a/lib/baobab/specification/from_openapi.v +++ b/lib/baobab/specification/from_openapi.v @@ -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 From b71362eb9a32f096a9bcccee6b2889a22e5bec60 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:22:36 +0100 Subject: [PATCH 5/8] fix encoding of non-generic jsonrpc response --- lib/schemas/jsonrpc/model_response.v | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/schemas/jsonrpc/model_response.v b/lib/schemas/jsonrpc/model_response.v index ac7c6b17..afcb357f 100644 --- a/lib/schemas/jsonrpc/model_response.v +++ b/lib/schemas/jsonrpc/model_response.v @@ -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. From bd5cafbad72efb74ec1e9efa91888b8df805b2e1 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:38:57 +0100 Subject: [PATCH 6/8] generator mcp integration progress --- lib/baobab/generator/generate_methods.v | 12 ++++++------ lib/baobab/generator/generate_methods_interface.v | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/baobab/generator/generate_methods.v b/lib/baobab/generator/generate_methods.v index 5f212381..72e2e702 100644 --- a/lib/baobab/generator/generate_methods.v +++ b/lib/baobab/generator/generate_methods.v @@ -13,15 +13,15 @@ import log const crud_prefixes = ['new', 'get', 'set', 'delete', 'list'] pub struct Source { - openapi_path string - openrpc_path string + openapi_path ?string + openrpc_path ?string } pub fn generate_methods_file_str(source Source) !string { - actor_spec := if source.openapi_path != "" { - specification.from_openapi(openapi.new(path: source.openapi_path)!)! - } else if source.openrpc_path != "" { - specification.from_openrpc(openrpc.new(path: source.openrpc_path)!)! + 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()! diff --git a/lib/baobab/generator/generate_methods_interface.v b/lib/baobab/generator/generate_methods_interface.v index 7492fe65..5163d909 100644 --- a/lib/baobab/generator/generate_methods_interface.v +++ b/lib/baobab/generator/generate_methods_interface.v @@ -8,10 +8,10 @@ import freeflowuniverse.herolib.schemas.openapi import freeflowuniverse.herolib.schemas.openrpc pub fn generate_methods_interface_file_str(source Source) !string { - actor_spec := if source.openapi_path != "" { - specification.from_openapi(openapi.new(path: source.openapi_path)!)! - } else if source.openrpc_path != "" { - specification.from_openrpc(openrpc.new(path: source.openrpc_path)!)! + 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()! From 86d47c218b871ede9a371730b612fdaba686fceb Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 28 Mar 2025 19:47:26 +0100 Subject: [PATCH 7/8] vcode and baobab mcps enhancements --- lib/baobab/generator/generate_methods.v | 19 ++-- .../generator/generate_methods_example.v | 11 +++ lib/baobab/generator/generate_model.v | 12 +++ lib/core/code/vlang_utils.v | 93 +++---------------- lib/mcp/baobab/baobab_tools.v | 66 +++++++++++++ lib/mcp/baobab/server.v | 4 + lib/mcp/vcode/command.v | 2 +- lib/mcp/vcode/server.v | 2 + lib/mcp/vcode/vlang_tools.v | 33 ++++--- lib/mcp/vcode/write_vfile_tool.v | 71 ++++++++++++++ lib/schemas/jsonschema/codegen/codegen.v | 1 + 11 files changed, 209 insertions(+), 105 deletions(-) create mode 100644 lib/mcp/vcode/write_vfile_tool.v diff --git a/lib/baobab/generator/generate_methods.v b/lib/baobab/generator/generate_methods.v index 72e2e702..9fcdca42 100644 --- a/lib/baobab/generator/generate_methods.v +++ b/lib/baobab/generator/generate_methods.v @@ -98,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{ diff --git a/lib/baobab/generator/generate_methods_example.v b/lib/baobab/generator/generate_methods_example.v index b0d996fe..3f303520 100644 --- a/lib/baobab/generator/generate_methods_example.v +++ b/lib/baobab/generator/generate_methods_example.v @@ -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) diff --git a/lib/baobab/generator/generate_model.v b/lib/baobab/generator/generate_model.v index d5be5c46..d8cba78f 100644 --- a/lib/baobab/generator/generate_model.v +++ b/lib/baobab/generator/generate_model.v @@ -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) diff --git a/lib/core/code/vlang_utils.v b/lib/core/code/vlang_utils.v index a3e715ff..cf4cf0cf 100644 --- a/lib/core/code/vlang_utils.v +++ b/lib/core/code/vlang_utils.v @@ -65,88 +65,17 @@ fn find_closing_brace(content string, start_i int) ?int { // 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) !string { -// log.error('Looking for function ${function_name} in file ${file_path}') -// 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 V file: ${file_path}: ${err}') -// } - -// for fn in vfile.functions() { -// if fn.name == function_name { -// return fn.code -// } -// } - -// return error('Function ${function_name} not found in ${file_path}') -// } - -// lines := content.split_into_lines() -// mut result := []string{} -// mut in_function := false -// mut brace_count := 0 -// mut comment_block := []string{} - -// for i, line in lines { -// trimmed := line.trim_space() - -// // Collect comments that might be above the function -// if trimmed.starts_with('//') { -// if !in_function { -// comment_block << line -// } else if brace_count > 0 { -// result << line -// } -// continue -// } - -// // Check if we found the function -// if !in_function && (trimmed.starts_with('fn ${function_name}(') -// || trimmed.starts_with('pub fn ${function_name}(')) { -// in_function = true -// // Add collected comments -// result << comment_block -// comment_block = [] -// result << line -// if line.contains('{') { -// brace_count++ -// } -// continue -// } - -// // If we're inside the function, keep track of braces -// if in_function { -// result << line - -// for c in line { -// if c == `{` { -// brace_count++ -// } else if c == `}` { -// brace_count-- -// } -// } - -// // If brace_count is 0, we've reached the end of the function -// if brace_count == 0 && trimmed.contains('}') { -// return result.join('\n') -// } -// } else { -// // Reset comment block if we pass a blank line -// if trimmed == '' { -// comment_block = [] -// } -// } -// } - -// if !in_function { -// return error('Function "${function_name}" not found in ${file_path}') -// } - -// return result.join('\n') -// } +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: diff --git a/lib/mcp/baobab/baobab_tools.v b/lib/mcp/baobab/baobab_tools.v index 7866c6f3..cfa09afa 100644 --- a/lib/mcp/baobab/baobab_tools.v +++ b/lib/mcp/baobab/baobab_tools.v @@ -98,4 +98,70 @@ pub fn (d &Baobab) generate_methods_interface_file_tool_handler(arguments map[st 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) + } } \ No newline at end of file diff --git a/lib/mcp/baobab/server.v b/lib/mcp/baobab/server.v index d309288e..cee8253a 100644 --- a/lib/mcp/baobab/server.v +++ b/lib/mcp/baobab/server.v @@ -16,11 +16,15 @@ pub fn new_mcp_server(v &Baobab) !&mcp.Server { '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{ diff --git a/lib/mcp/vcode/command.v b/lib/mcp/vcode/command.v index 2780b6a6..f4175a70 100644 --- a/lib/mcp/vcode/command.v +++ b/lib/mcp/vcode/command.v @@ -2,7 +2,7 @@ module vcode import cli -const command := cli.Command{ +pub const command := cli.Command{ sort_flags: true name: 'vcode' execute: cmd_vcode diff --git a/lib/mcp/vcode/server.v b/lib/mcp/vcode/server.v index 64a0f5a0..92b63139 100644 --- a/lib/mcp/vcode/server.v +++ b/lib/mcp/vcode/server.v @@ -15,9 +15,11 @@ pub fn new_mcp_server(v &VCode) !&mcp.Server { mut server := mcp.new_server(mcp.MemoryBackend{ tools: { 'get_function_from_file': get_function_from_file_tool + 'write_vfile': write_vfile_tool } tool_handlers: { 'get_function_from_file': v.get_function_from_file_tool_handler + 'write_vfile': v.write_vfile_tool_handler } }, mcp.ServerParams{ config: mcp.ServerConfiguration{ diff --git a/lib/mcp/vcode/vlang_tools.v b/lib/mcp/vcode/vlang_tools.v index f53b5cd8..930bfa54 100644 --- a/lib/mcp/vcode/vlang_tools.v +++ b/lib/mcp/vcode/vlang_tools.v @@ -1,6 +1,9 @@ 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' @@ -12,23 +15,25 @@ RETURNS: string - the function block including comments, or empty string if not input_schema: jsonschema.Schema{ typ: 'object' properties: { - 'file_path': jsonschema.Schema{ + 'file_path': jsonschema.SchemaRef(jsonschema.Schema{ typ: 'string' - items: mcp.ToolItems{ - typ: '' - enum: [] - } - enum: [] - } - 'function_name': jsonschema.Schema{ + }) + 'function_name': jsonschema.SchemaRef(jsonschema.Schema{ typ: 'string' - items: mcp.ToolItems{ - typ: '' - enum: [] - } - enum: [] - } + }) } 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()) + } +} diff --git a/lib/mcp/vcode/write_vfile_tool.v b/lib/mcp/vcode/write_vfile_tool.v new file mode 100644 index 00000000..861e652e --- /dev/null +++ b/lib/mcp/vcode/write_vfile_tool.v @@ -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}') + } +} diff --git a/lib/schemas/jsonschema/codegen/codegen.v b/lib/schemas/jsonschema/codegen/codegen.v index 4a832bb1..3d5d1c5f 100644 --- a/lib/schemas/jsonschema/codegen/codegen.v +++ b/lib/schemas/jsonschema/codegen/codegen.v @@ -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 } From f99419371a81d46f09bf6b91cd74db083b64ba2e Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 28 Mar 2025 20:52:19 +0100 Subject: [PATCH 8/8] progress in mail code gen --- lib/circles/mail/methods_.v | 58 ++ lib/circles/mail/methods_example2_.v | 55 ++ lib/circles/mail/methods_example_.v | 295 +++++++ lib/circles/mail/methods_interface_.v | 18 + lib/circles/mail/model_.v | 59 ++ lib/circles/mail/openapi.json | 1019 +++++++++++++++++++++++++ lib/circles/mail/openrpc/openrpc.json | 841 ++++++++++++++++++++ 7 files changed, 2345 insertions(+) create mode 100644 lib/circles/mail/methods_.v create mode 100644 lib/circles/mail/methods_example2_.v create mode 100644 lib/circles/mail/methods_example_.v create mode 100644 lib/circles/mail/methods_interface_.v create mode 100644 lib/circles/mail/model_.v create mode 100644 lib/circles/mail/openapi.json create mode 100644 lib/circles/mail/openrpc/openrpc.json diff --git a/lib/circles/mail/methods_.v b/lib/circles/mail/methods_.v new file mode 100644 index 00000000..3d19c6e8 --- /dev/null +++ b/lib/circles/mail/methods_.v @@ -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') +} \ No newline at end of file diff --git a/lib/circles/mail/methods_example2_.v b/lib/circles/mail/methods_example2_.v new file mode 100644 index 00000000..e5bc93bf --- /dev/null +++ b/lib/circles/mail/methods_example2_.v @@ -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)! +} \ No newline at end of file diff --git a/lib/circles/mail/methods_example_.v b/lib/circles/mail/methods_example_.v new file mode 100644 index 00000000..0c9982d9 --- /dev/null +++ b/lib/circles/mail/methods_example_.v @@ -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: '' + } + } + + 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: '' + } + } + + // 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: '' + } + } +} + +// 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: '' + } + } + } + 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: '' + } + } + } + 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: '' + } + } + ] +} + +// 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: '' + } + }, + 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: '' + } + } + ] + } + 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: '' + } + } + } + return error('Email not found') +} \ No newline at end of file diff --git a/lib/circles/mail/methods_interface_.v b/lib/circles/mail/methods_interface_.v new file mode 100644 index 00000000..425ed096 --- /dev/null +++ b/lib/circles/mail/methods_interface_.v @@ -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 +} \ No newline at end of file diff --git a/lib/circles/mail/model_.v b/lib/circles/mail/model_.v new file mode 100644 index 00000000..f9fcefc8 --- /dev/null +++ b/lib/circles/mail/model_.v @@ -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 +} \ No newline at end of file diff --git a/lib/circles/mail/openapi.json b/lib/circles/mail/openapi.json new file mode 100644 index 00000000..8b7d1206 --- /dev/null +++ b/lib/circles/mail/openapi.json @@ -0,0 +1,1019 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "HeroLib Circles Mail API", + "description": "API for Mail functionality of HeroLib Circles MCC module.\nThis 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", + "description": "Production server" + }, + { + "url": "https://dev-api.example.com/v1", + "description": "Development server" + } + ], + "paths": { + "/emails": { + "get": { + "summary": "List all emails", + "description": "Returns a list of all emails in the system", + "operationId": "listEmails", + "parameters": [ + { + "name": "mailbox", + "in": "query", + "description": "Filter emails by mailbox", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A list of emails", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Email" + } + }, + "examples": { + "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": "" + } + }, + { + "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": "" + } + } + ] + } + } + } + } + } + } + }, + "post": { + "summary": "Create a new email", + "description": "Creates a new email in the system", + "operationId": "createEmail", + "requestBody": { + "description": "Email to create", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailCreate" + }, + "examples": { + "createEmail": { + "value": { + "mailbox": "INBOX", + "message": "Hello, this is a new email.", + "attachments": [], + "flags": [], + "envelope": { + "subject": "New Email", + "from": [ + "sender@example.com" + ], + "to": [ + "recipient@example.com" + ], + "cc": [], + "bcc": [] + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created email", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + }, + "examples": { + "createdEmail": { + "value": { + "id": 3, + "uid": 103, + "seq_num": 3, + "mailbox": "INBOX", + "message": "Hello, this is a new email.", + "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": "" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid email data" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/emails/{id}": { + "get": { + "summary": "Get email by ID", + "description": "Returns a single email by ID", + "operationId": "getEmailById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Email ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint32" + } + } + ], + "responses": { + "200": { + "description": "Email found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + }, + "examples": { + "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": "" + } + } + } + } + } + } + }, + "404": { + "description": "Email not found" + }, + "500": { + "description": "Internal server error" + } + } + }, + "put": { + "summary": "Update email", + "description": "Updates an existing email", + "operationId": "updateEmail", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Email ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint32" + } + } + ], + "requestBody": { + "description": "Updated email data", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + }, + "examples": { + "updateEmail": { + "value": { + "mailbox": "INBOX", + "message": "This is an updated email message.", + "attachments": [], + "flags": ["\\Seen", "\\Flagged"], + "envelope": { + "subject": "Updated Email Subject", + "from": ["sender@example.com"], + "to": ["recipient@example.com"], + "cc": ["cc@example.com"], + "bcc": [] + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Email updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + }, + "examples": { + "updatedEmail": { + "value": { + "id": 1, + "uid": 101, + "seq_num": 1, + "mailbox": "INBOX", + "message": "This is an updated email message.", + "attachments": [], + "flags": ["\\Seen", "\\Flagged"], + "internal_date": 1647356400, + "size": 300, + "envelope": { + "date": 1647356400, + "subject": "Updated Email Subject", + "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": "" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid email data" + }, + "404": { + "description": "Email not found" + }, + "500": { + "description": "Internal server error" + } + } + }, + "delete": { + "summary": "Delete email", + "description": "Deletes an email", + "operationId": "deleteEmail", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Email ID", + "required": true, + "schema": { + "type": "integer", + "format": "uint32" + } + } + ], + "responses": { + "204": { + "description": "Email deleted" + }, + "404": { + "description": "Email not found" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/emails/search": { + "get": { + "summary": "Search emails", + "description": "Search for emails by various criteria", + "operationId": "searchEmails", + "parameters": [ + { + "name": "subject", + "in": "query", + "description": "Search in email subject", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "description": "Search by sender email", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "to", + "in": "query", + "description": "Search by recipient email", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "content", + "in": "query", + "description": "Search in email content", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "date_from", + "in": "query", + "description": "Start date (unix timestamp)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "date_to", + "in": "query", + "description": "End date (unix timestamp)", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "has_attachments", + "in": "query", + "description": "Filter emails with attachments", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Search results", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Email" + } + }, + "examples": { + "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": "" + } + } + ] + } + } + } + } + }, + "400": { + "description": "Invalid search parameters" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/emails/mailbox/{mailbox}": { + "get": { + "summary": "Get emails by mailbox", + "description": "Returns all emails in a specific mailbox", + "operationId": "getEmailsByMailbox", + "parameters": [ + { + "name": "mailbox", + "in": "path", + "description": "Mailbox name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of emails in the mailbox", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Email" + } + }, + "examples": { + "mailboxEmails": { + "value": [ + { + "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": { + "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": "" + } + }, + { + "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": { + "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": "" + } + } + ] + } + } + } + } + }, + "404": { + "description": "Mailbox not found" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/emails/flags/{uid}": { + "put": { + "summary": "Update email flags", + "description": "Update the flags of an email by its UID", + "operationId": "updateEmailFlags", + "parameters": [ + { + "name": "uid", + "in": "path", + "description": "Email UID", + "required": true, + "schema": { + "type": "integer", + "format": "uint32" + } + } + ], + "requestBody": { + "description": "Flags to update", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "flags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "\\Seen", + "\\Flagged" + ] + } + }, + "required": [ + "flags" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Flags updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + }, + "examples": { + "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": "" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid flags" + }, + "404": { + "description": "Email not found" + }, + "500": { + "description": "Internal server error" + } + } + } + } + }, + "components": { + "schemas": { + "Email": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "description": "Database ID (assigned by DBHandler)", + "example": 1 + }, + "uid": { + "type": "integer", + "format": "uint32", + "description": "Unique identifier of the message (in the circle)", + "example": 101 + }, + "seq_num": { + "type": "integer", + "format": "uint32", + "description": "IMAP sequence number (in the mailbox)", + "example": 1 + }, + "mailbox": { + "type": "string", + "description": "The mailbox this email belongs to", + "example": "INBOX" + }, + "message": { + "type": "string", + "description": "The email body content", + "example": "Hello, this is a test email." + }, + "attachments": { + "type": "array", + "description": "Any file attachments", + "items": { + "$ref": "#/components/schemas/Attachment" + } + }, + "flags": { + "type": "array", + "description": "IMAP flags like \\Seen, \\Deleted, etc.", + "items": { + "type": "string" + }, + "example": [ + "\\Seen" + ] + }, + "internal_date": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp when the email was received", + "example": 1647356400 + }, + "size": { + "type": "integer", + "format": "uint32", + "description": "Size of the message in bytes", + "example": 256 + }, + "envelope": { + "$ref": "#/components/schemas/Envelope" + } + }, + "required": [ + "id", + "uid", + "mailbox", + "message" + ] + }, + "EmailCreate": { + "type": "object", + "properties": { + "mailbox": { + "type": "string", + "description": "The mailbox this email belongs to", + "example": "INBOX" + }, + "message": { + "type": "string", + "description": "The email body content", + "example": "Hello, this is a new email." + }, + "attachments": { + "type": "array", + "description": "Any file attachments", + "items": { + "$ref": "#/components/schemas/Attachment" + } + }, + "flags": { + "type": "array", + "description": "IMAP flags like \\Seen, \\Deleted, etc.", + "items": { + "type": "string" + }, + "example": [] + }, + "envelope": { + "$ref": "#/components/schemas/EnvelopeCreate" + } + }, + "required": [ + "mailbox", + "message" + ] + }, + "EmailUpdate": { + "type": "object", + "properties": { + "mailbox": { + "type": "string", + "description": "The mailbox this email belongs to" + }, + "message": { + "type": "string", + "description": "The email body content" + }, + "attachments": { + "type": "array", + "description": "Any file attachments", + "items": { + "$ref": "#/components/schemas/Attachment" + } + }, + "flags": { + "type": "array", + "description": "IMAP flags like \\Seen, \\Deleted, etc.", + "items": { + "type": "string" + }, + "example": [ + "\\Seen", + "\\Flagged" + ] + }, + "envelope": { + "$ref": "#/components/schemas/EnvelopeCreate" + } + } + }, + "Attachment": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "description": "Name of the attached file", + "example": "document.pdf" + }, + "content_type": { + "type": "string", + "description": "MIME type of the attachment", + "example": "application/pdf" + }, + "data": { + "type": "string", + "description": "Base64 encoded binary data", + "example": "base64encodeddata" + } + }, + "required": [ + "filename", + "content_type", + "data" + ] + }, + "Envelope": { + "type": "object", + "properties": { + "date": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp of the email date", + "example": 1647356400 + }, + "subject": { + "type": "string", + "description": "Email subject", + "example": "Test Email" + }, + "from": { + "type": "array", + "description": "From addresses", + "items": { + "type": "string" + }, + "example": [ + "sender@example.com" + ] + }, + "sender": { + "type": "array", + "description": "Sender addresses", + "items": { + "type": "string" + }, + "example": [ + "sender@example.com" + ] + }, + "reply_to": { + "type": "array", + "description": "Reply-To addresses", + "items": { + "type": "string" + }, + "example": [ + "sender@example.com" + ] + }, + "to": { + "type": "array", + "description": "To addresses", + "items": { + "type": "string" + }, + "example": [ + "recipient@example.com" + ] + }, + "cc": { + "type": "array", + "description": "CC addresses", + "items": { + "type": "string" + }, + "example": [] + }, + "bcc": { + "type": "array", + "description": "BCC addresses", + "items": { + "type": "string" + }, + "example": [] + }, + "in_reply_to": { + "type": "string", + "description": "Message ID this email is replying to", + "example": "" + }, + "message_id": { + "type": "string", + "description": "Unique message ID", + "example": "" + } + }, + "required": [ + "subject", + "from", + "to" + ] + }, + "EnvelopeCreate": { + "type": "object", + "properties": { + "subject": { + "type": "string", + "description": "Email subject", + "example": "New Email" + }, + "from": { + "type": "array", + "description": "From addresses", + "items": { + "type": "string" + }, + "example": [ + "sender@example.com" + ] + }, + "to": { + "type": "array", + "description": "To addresses", + "items": { + "type": "string" + }, + "example": [ + "recipient@example.com" + ] + }, + "cc": { + "type": "array", + "description": "CC addresses", + "items": { + "type": "string" + }, + "example": [] + }, + "bcc": { + "type": "array", + "description": "BCC addresses", + "items": { + "type": "string" + }, + "example": [] + } + }, + "required": [ + "subject", + "from", + "to" + ] + } + } + } +} diff --git a/lib/circles/mail/openrpc/openrpc.json b/lib/circles/mail/openrpc/openrpc.json new file mode 100644 index 00000000..80837e73 --- /dev/null +++ b/lib/circles/mail/openrpc/openrpc.json @@ -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": "" + } + }, + { + "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": "" + } + } + ] + } + ] + } + }, + { + "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": "" + } + } + } + ] + } + }, + { + "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": "" + } + } + } + ] + } + }, + { + "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": "" + } + } + } + ] + } + }, + { + "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": "" + } + } + ] + } + ] + } + }, + { + "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": "" + } + }, + { + "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": "" + } + } + ] + } + ] + } + }, + { + "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": "" + } + } + } + ] + } + } + ], + "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"] + } + } + } +}