merge v_do into overarching developer mcp project

This commit is contained in:
Timur Gordon
2025-03-14 23:06:34 +01:00
parent 8b9b0678b8
commit 02e4ea180d
15 changed files with 598 additions and 110 deletions

View File

@@ -0,0 +1,4 @@
module developer
pub struct Developer {}

View File

@@ -0,0 +1,304 @@
module developer
import freeflowuniverse.herolib.mcp
// create_mcp_tool parses a V language function string and returns an MCP Tool struct
// function: The V function string including preceding comments
// types: A map of struct names to their definitions for complex parameter types
pub fn (d Developer) create_mcp_tool(function string, types map[string]string) !mcp.Tool {
// Extract description from preceding comments
mut description := ''
lines := function.split('\n')
// Find function signature line
mut fn_line_idx := -1
for i, line in lines {
if line.trim_space().starts_with('fn ') || line.trim_space().starts_with('pub fn ') {
fn_line_idx = i
break
}
}
if fn_line_idx == -1 {
return error('Invalid function: no function signature found')
}
// Extract comments before the function
for i := 0; i < fn_line_idx; i++ {
line := lines[i].trim_space()
if line.starts_with('//') {
// Remove the comment marker and any leading space
comment := line[2..].trim_space()
if description != '' {
description += '\n'
}
description += comment
}
}
// Parse function signature
fn_signature := lines[fn_line_idx].trim_space()
// Extract function name
mut fn_name := ''
// Check if this is a method with a receiver
if fn_signature.contains('fn (') {
// This is a method with a receiver
// Format: [pub] fn (receiver Type) name(...)
// Find the closing parenthesis of the receiver
mut receiver_end := fn_signature.index(')') or { return error('Invalid method signature: missing closing parenthesis for receiver') }
// Extract the text after the receiver
mut after_receiver := fn_signature[receiver_end + 1..].trim_space()
// Extract the function name (everything before the opening parenthesis)
mut params_start := after_receiver.index('(') or { return error('Invalid method signature: missing parameters') }
fn_name = after_receiver[0..params_start].trim_space()
} else if fn_signature.starts_with('pub fn ') {
// Regular public function
mut prefix_len := 'pub fn '.len
mut params_start := fn_signature.index('(') or { return error('Invalid function signature: missing parameters') }
fn_name = fn_signature[prefix_len..params_start].trim_space()
} else if fn_signature.starts_with('fn ') {
// Regular function
mut prefix_len := 'fn '.len
mut params_start := fn_signature.index('(') or { return error('Invalid function signature: missing parameters') }
fn_name = fn_signature[prefix_len..params_start].trim_space()
} else {
return error('Invalid function signature: must start with "fn" or "pub fn"')
}
if fn_name == '' {
return error('Could not extract function name')
}
// Extract parameters
mut params_str := ''
// Check if this is a method with a receiver
if fn_signature.contains('fn (') {
// This is a method with a receiver
// Find the closing parenthesis of the receiver
mut receiver_end := fn_signature.index(')') or { return error('Invalid method signature: missing closing parenthesis for receiver') }
// Find the opening parenthesis of the parameters
mut params_start := -1
for i := receiver_end + 1; i < fn_signature.len; i++ {
if fn_signature[i] == `(` {
params_start = i
break
}
}
if params_start == -1 {
return error('Invalid method signature: missing parameter list')
}
// Find the closing parenthesis of the parameters
mut params_end := fn_signature.last_index(')') or { return error('Invalid method signature: missing closing parenthesis for parameters') }
// Extract the parameters
params_str = fn_signature[params_start + 1..params_end].trim_space()
} else {
// Regular function
mut params_start := fn_signature.index('(') or { return error('Invalid function signature: missing parameters') }
mut params_end := fn_signature.last_index(')') or { return error('Invalid function signature: missing closing parenthesis') }
// Extract the parameters
params_str = fn_signature[params_start + 1..params_end].trim_space()
}
// Create input schema for parameters
mut properties := map[string]mcp.ToolProperty{}
mut required := []string{}
if params_str != '' {
param_list := params_str.split(',')
for param in param_list {
trimmed_param := param.trim_space()
if trimmed_param == '' {
continue
}
// Split parameter into name and type
param_parts := trimmed_param.split_any(' \t')
if param_parts.len < 2 {
continue
}
param_name := param_parts[0]
param_type := param_parts[1]
// Add to required parameters
required << param_name
// Create property for this parameter
mut property := mcp.ToolProperty{}
// Check if this is a complex type defined in the types map
if param_type in types {
// Parse the struct definition to create a nested schema
struct_def := types[param_type]
struct_schema := d.create_mcp_tool_input_schema(struct_def)!
property = mcp.ToolProperty{
typ: struct_schema.typ
}
} else {
// Handle primitive types
schema := d.create_mcp_tool_input_schema(param_type)!
property = mcp.ToolProperty{
typ: schema.typ
}
}
properties[param_name] = property
}
}
// Create the input schema
input_schema := mcp.ToolInputSchema{
typ: 'object',
properties: properties,
required: required
}
// Create and return the Tool
return mcp.Tool{
name: fn_name,
description: description,
input_schema: input_schema
}
}
// create_mcp_tool_input_schema creates a ToolInputSchema for a given input type
// input: The input type string
// returns: A ToolInputSchema for the given input type
// errors: Returns an error if the input type is not supported
pub fn (d Developer) create_mcp_tool_input_schema(input string) !mcp.ToolInputSchema {
// if input is a primitive type, return a mcp ToolInputSchema with that type
if input == 'string' {
return mcp.ToolInputSchema{
typ: 'string'
}
} else if input == 'int' {
return mcp.ToolInputSchema{
typ: 'integer'
}
} else if input == 'float' {
return mcp.ToolInputSchema{
typ: 'number'
}
} else if input == 'bool' {
return mcp.ToolInputSchema{
typ: 'boolean'
}
}
// if input is a struct, return a mcp ToolInputSchema with typ 'object' and properties for each field in the struct
if input.starts_with('pub struct ') {
struct_name := input[11..].split(' ')[0]
fields := parse_struct_fields(input)
mut properties := map[string]mcp.ToolProperty{}
for field_name, field_type in fields {
property := mcp.ToolProperty{
typ: d.create_mcp_tool_input_schema(field_type)!.typ
}
properties[field_name] = property
}
return mcp.ToolInputSchema{
typ: 'object',
properties: properties
}
}
// if input is an array, return a mcp ToolInputSchema with typ 'array' and items of the item type
if input.starts_with('[]') {
item_type := input[2..]
// For array types, we create a schema with type 'array'
// The actual item type is determined by the primitive type
mut item_type_str := 'string' // default
if item_type == 'int' {
item_type_str = 'integer'
} else if item_type == 'float' {
item_type_str = 'number'
} else if item_type == 'bool' {
item_type_str = 'boolean'
}
// Create a property for the array items
mut property := mcp.ToolProperty{
typ: 'array'
}
// Add the property to the schema
mut properties := map[string]mcp.ToolProperty{}
properties['items'] = property
return mcp.ToolInputSchema{
typ: 'array',
properties: properties
}
}
// Default to string type for unknown types
return mcp.ToolInputSchema{
typ: 'string'
}
}
// parse_struct_fields parses a V language struct definition string and returns a map of field names to their types
fn parse_struct_fields(struct_def string) map[string]string {
mut fields := map[string]string{}
// Find the opening and closing braces of the struct definition
start_idx := struct_def.index('{') or { return fields }
end_idx := struct_def.last_index('}') or { return fields }
// Extract the content between the braces
struct_content := struct_def[start_idx + 1..end_idx].trim_space()
// Split the content by newlines to get individual field definitions
field_lines := struct_content.split('
')
for line in field_lines {
trimmed_line := line.trim_space()
// Skip empty lines and comments
if trimmed_line == '' || trimmed_line.starts_with('//') {
continue
}
// Handle pub: or mut: prefixes
mut field_def := trimmed_line
if field_def.starts_with('pub:') || field_def.starts_with('mut:') {
field_def = field_def.all_after(':').trim_space()
}
// Split by whitespace to separate field name and type
parts := field_def.split_any(' ')
if parts.len < 2 {
continue
}
field_name := parts[0]
field_type := parts[1..].join(' ')
// Handle attributes like @[json: 'name']
if field_name.contains('@[') {
continue
}
fields[field_name] = field_type
}
return fields
}

View File

@@ -0,0 +1,175 @@
module developer
import freeflowuniverse.herolib.mcp
import json
fn test_parse_struct_fields() {
// Test case 1: Simple struct with primitive types
simple_struct := 'pub struct User {
name string
age int
active bool
}'
fields := parse_struct_fields(simple_struct)
assert fields.len == 3
assert fields['name'] == 'string'
assert fields['age'] == 'int'
assert fields['active'] == 'bool'
// Test case 2: Struct with pub: and mut: sections
complex_struct := 'pub struct Config {
pub:
host string
port int
mut:
connected bool
retries int
}'
fields2 := parse_struct_fields(complex_struct)
assert fields2.len == 4
assert fields2['host'] == 'string'
assert fields2['port'] == 'int'
assert fields2['connected'] == 'bool'
assert fields2['retries'] == 'int'
// Test case 3: Struct with attributes and comments
struct_with_attrs := 'pub struct ApiResponse {
// User ID
id int
// User\'s full name
name string @[json: "full_name"]
// Whether account is active
active bool
}'
fields3 := parse_struct_fields(struct_with_attrs)
assert fields3.len == 3 // All fields are included
assert fields3['id'] == 'int'
assert fields3['active'] == 'bool'
// Test case 4: Empty struct
empty_struct := 'pub struct Empty {}'
fields4 := parse_struct_fields(empty_struct)
assert fields4.len == 0
println('test_parse_struct_fields passed')
}
fn test_create_mcp_tool_input_schema() {
d := Developer{}
// Test case 1: Primitive types
string_schema := d.create_mcp_tool_input_schema('string') or { panic(err) }
assert string_schema.typ == 'string'
int_schema := d.create_mcp_tool_input_schema('int') or { panic(err) }
assert int_schema.typ == 'integer'
float_schema := d.create_mcp_tool_input_schema('float') or { panic(err) }
assert float_schema.typ == 'number'
bool_schema := d.create_mcp_tool_input_schema('bool') or { panic(err) }
assert bool_schema.typ == 'boolean'
// Test case 2: Array type
array_schema := d.create_mcp_tool_input_schema('[]string') or { panic(err) }
assert array_schema.typ == 'array'
// In our implementation, arrays don't have items directly in the schema
// Test case 3: Struct type
struct_def := 'pub struct Person {
name string
age int
}'
struct_schema := d.create_mcp_tool_input_schema(struct_def) or { panic(err) }
assert struct_schema.typ == 'object'
assert struct_schema.properties.len == 2
assert struct_schema.properties['name'].typ == 'string'
assert struct_schema.properties['age'].typ == 'integer'
println('test_create_mcp_tool_input_schema passed')
}
fn test_create_mcp_tool() {
d := Developer{}
// Test case 1: Simple function with primitive types
simple_fn := '// Get user by ID
// Returns user information
pub fn get_user(id int, include_details bool) {
// Implementation
}'
tool1 := d.create_mcp_tool(simple_fn, {}) or { panic(err) }
assert tool1.name == 'get_user'
expected_desc1 := "Get user by ID\nReturns user information"
assert tool1.description == expected_desc1
assert tool1.input_schema.typ == 'object'
assert tool1.input_schema.properties.len == 2
assert tool1.input_schema.properties['id'].typ == 'integer'
assert tool1.input_schema.properties['include_details'].typ == 'boolean'
assert tool1.input_schema.required.len == 2
assert 'id' in tool1.input_schema.required
assert 'include_details' in tool1.input_schema.required
// Test case 2: Method with receiver
method_fn := '// Update user profile
pub fn (u User) update_profile(name string, age int) bool {
// Implementation
return true
}'
tool2 := d.create_mcp_tool(method_fn, {}) or { panic(err) }
assert tool2.name == 'update_profile'
assert tool2.description == 'Update user profile'
assert tool2.input_schema.properties.len == 2
assert tool2.input_schema.properties['name'].typ == 'string'
assert tool2.input_schema.properties['age'].typ == 'integer'
// Test case 3: Function with complex types
complex_fn := '// Create new configuration
// Sets up system configuration
fn create_config(name string, settings Config) !Config {
// Implementation
}'
config_struct := 'pub struct Config {
server_url string
max_retries int
timeout float
}'
tool3 := d.create_mcp_tool(complex_fn, {'Config': config_struct}) or { panic(err) }
assert tool3.name == 'create_config'
expected_desc3 := "Create new configuration\nSets up system configuration"
assert tool3.description == expected_desc3
assert tool3.input_schema.properties.len == 2
assert tool3.input_schema.properties['name'].typ == 'string'
assert tool3.input_schema.properties['settings'].typ == 'object'
// Test case 4: Function with no parameters
no_params_fn := '// Initialize system
pub fn initialize() {
// Implementation
}'
tool4 := d.create_mcp_tool(no_params_fn, {}) or { panic(err) }
assert tool4.name == 'initialize'
assert tool4.description == 'Initialize system'
assert tool4.input_schema.properties.len == 0
assert tool4.input_schema.required.len == 0
println('test_create_mcp_tool passed')
}
// Run all tests
fn main() {
test_parse_struct_fields()
test_create_mcp_tool_input_schema()
test_create_mcp_tool()
println('All tests passed successfully!')
}

View File

@@ -0,0 +1,21 @@
module developer
import freeflowuniverse.herolib.mcp
// Tool definition for the create_mcp_tool function
const create_mcp_tool_tool = mcp.Tool{
name: 'create_mcp_tool'
description: 'Parses a V language function string and returns an MCP Tool struct. This tool analyzes function signatures, extracts parameters, and generates the appropriate MCP Tool representation.'
input_schema: mcp.ToolInputSchema{
typ: 'object'
properties: {
'function': mcp.ToolProperty{
typ: 'string'
}
'types': mcp.ToolProperty{
typ: 'object'
}
}
required: ['function']
}
}

0
lib/mcp/developer/mcp.v Normal file
View File

88
lib/mcp/developer/vlang.v Normal file
View File

@@ -0,0 +1,88 @@
module developer
import freeflowuniverse.herolib.mcp
import os
// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v
fn list_v_files(dir string) ![]string {
files := os.ls(dir) or {
return error('Error listing directory: $err')
}
mut v_files := []string{}
for file in files {
if file.ends_with('.v') && !file.ends_with('_.v') {
filepath := os.join_path(dir, file)
v_files << filepath
}
}
return v_files
}
// test runs v test on the specified file or directory
pub fn vtest(fullpath string) !string {
logger.info('test $fullpath')
if !os.exists(fullpath) {
return error('File or directory does not exist: $fullpath')
}
if os.is_dir(fullpath) {
mut results:=""
for item in list_v_files(fullpath)!{
results += vtest(item)!
results += '\n-----------------------\n'
}
return results
}else{
cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}'
logger.debug('Executing command: $cmd')
result := os.execute(cmd)
if result.exit_code != 0 {
return error('Test failed for $fullpath with exit code ${result.exit_code}\n${result.output}')
} else {
logger.info('Test completed for $fullpath')
}
return 'Command: $cmd\nExit code: ${result.exit_code}\nOutput:\n${result.output}'
}
}
// vvet runs v vet on the specified file or directory
pub fn vvet(fullpath string) !string {
logger.info('vet $fullpath')
if !os.exists(fullpath) {
return error('File or directory does not exist: $fullpath')
}
if os.is_dir(fullpath) {
mut results := ""
files := list_v_files(fullpath) or {
return error('Error listing V files: $err')
}
for file in files {
results += vet_file(file) or {
logger.error('Failed to vet $file: $err')
return error('Failed to vet $file: $err')
}
results += '\n-----------------------\n'
}
return results
} else {
return vet_file(fullpath)
}
}
// vet_file runs v vet on a single file
fn vet_file(file string) !string {
cmd := 'v vet -v -w ${file}'
logger.debug('Executing command: $cmd')
result := os.execute(cmd)
if result.exit_code != 0 {
return error('Vet failed for $file with exit code ${result.exit_code}\n${result.output}')
} else {
logger.info('Vet completed for $file')
}
return 'Command: $cmd\nExit code: ${result.exit_code}\nOutput:\n${result.output}'
}
// cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath}'

View File

@@ -1,4 +0,0 @@
module handlers
import os
import freeflowuniverse.herolib.mcp.v_do.logger

View File

@@ -1,21 +0,0 @@
module handlers
import os
import freeflowuniverse.herolib.mcp.v_do.logger
// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v
fn list_v_files(dir string) ![]string {
files := os.ls(dir) or {
return error('Error listing directory: $err')
}
mut v_files := []string{}
for file in files {
if file.ends_with('.v') && !file.ends_with('_.v') {
filepath := os.join_path(dir, file)
v_files << filepath
}
}
return v_files
}

View File

@@ -1,8 +0,0 @@
module handlers
import os
import freeflowuniverse.herolib.mcp.v_do.logger
// cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath}'

View File

@@ -1,31 +0,0 @@
module handlers
import os
import freeflowuniverse.herolib.mcp.v_do.logger
// test runs v test on the specified file or directory
pub fn vtest(fullpath string) !string {
logger.info('test $fullpath')
if !os.exists(fullpath) {
return error('File or directory does not exist: $fullpath')
}
if os.is_dir(fullpath) {
mut results:=""
for item in list_v_files(fullpath)!{
results += vtest(item)!
results += '\n-----------------------\n'
}
return results
}else{
cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}'
logger.debug('Executing command: $cmd')
result := os.execute(cmd)
if result.exit_code != 0 {
return error('Test failed for $fullpath with exit code ${result.exit_code}\n${result.output}')
} else {
logger.info('Test completed for $fullpath')
}
return 'Command: $cmd\nExit code: ${result.exit_code}\nOutput:\n${result.output}'
}
}

View File

@@ -1,42 +0,0 @@
module handlers
import os
import freeflowuniverse.herolib.mcp.v_do.logger
// vvet runs v vet on the specified file or directory
pub fn vvet(fullpath string) !string {
logger.info('vet $fullpath')
if !os.exists(fullpath) {
return error('File or directory does not exist: $fullpath')
}
if os.is_dir(fullpath) {
mut results := ""
files := list_v_files(fullpath) or {
return error('Error listing V files: $err')
}
for file in files {
results += vet_file(file) or {
logger.error('Failed to vet $file: $err')
return error('Failed to vet $file: $err')
}
results += '\n-----------------------\n'
}
return results
} else {
return vet_file(fullpath)
}
}
// vet_file runs v vet on a single file
fn vet_file(file string) !string {
cmd := 'v vet -v -w ${file}'
logger.debug('Executing command: $cmd')
result := os.execute(cmd)
if result.exit_code != 0 {
return error('Vet failed for $file with exit code ${result.exit_code}\n${result.output}')
} else {
logger.info('Vet completed for $file')
}
return 'Command: $cmd\nExit code: ${result.exit_code}\nOutput:\n${result.output}'
}

Binary file not shown.

View File

@@ -12,15 +12,17 @@ fn main() {
// Initialize the server with the empty handlers map
mut server := mcp.new_server(
handlers,
mcp.ServerConfiguration{
mcp.MemoryBackend{},
mcp.ServerParams{
handlers: handlers,
config:mcp.ServerConfiguration{
server_info: mcp.ServerInfo{
name: 'v_do'
version: '1.0.0'
}
}
}}
)!
server.start() or {
logger.fatal('Error starting server: $err')
exit(1)