reorganize and sort mcps

This commit is contained in:
Timur Gordon
2025-03-28 12:20:56 +01:00
parent bf26b0af1d
commit 186c3aae59
44 changed files with 1163 additions and 1480 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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}'

View File

@@ -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

View File

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

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

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

View File

@@ -1,40 +1,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)
}
}

View File

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

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

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

View File

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

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

@@ -0,0 +1,128 @@
module baobab
import freeflowuniverse.herolib.mcp
import freeflowuniverse.herolib.schemas.jsonrpc
import json
import x.json2
// This file contains tests for the Baobab MCP server implementation.
// It tests the server's ability to initialize and handle tool calls.
// test_new_mcp_server tests the creation of a new MCP server for the Baobab module
fn test_new_mcp_server() {
// Create a new MCP server
mut server := new_mcp_server() or {
assert false, 'Failed to create MCP server: ${err}'
return
}
// Verify server info
assert server.server_info.name == 'developer', 'Server name should be "developer"'
assert server.server_info.version == '1.0.0', 'Server version should be 1.0.0'
// Verify server capabilities
assert server.capabilities.prompts.list_changed == true, 'Prompts capability should have list_changed set to true'
assert server.capabilities.resources.subscribe == true, 'Resources capability should have subscribe set to true'
assert server.capabilities.resources.list_changed == true, 'Resources capability should have list_changed set to true'
assert server.capabilities.tools.list_changed == true, 'Tools capability should have list_changed set to true'
}
// test_mcp_server_initialize tests the initialize handler with a sample initialize request
fn test_mcp_server_initialize() {
// Create a new MCP server
mut server := new_mcp_server() or {
assert false, 'Failed to create MCP server: ${err}'
return
}
// Sample initialize request from the MCP specification
initialize_request := '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"sampling":{},"roots":{"listChanged":true}},"clientInfo":{"name":"mcp-inspector","version":"0.0.1"}}}'
// Process the request through the handler
response := server.handler.handle(initialize_request) or {
assert false, 'Handler failed to process request: ${err}'
return
}
// Decode the response to verify its structure
decoded_response := jsonrpc.decode_response(response) or {
assert false, 'Failed to decode response: ${err}'
return
}
// Verify that the response is not an error
assert !decoded_response.is_error(), 'Response should not be an error'
// Parse the result to verify its contents
result_json := decoded_response.result() or {
assert false, 'Failed to get result: ${err}'
return
}
// Decode the result into an ServerConfiguration struct
result := json.decode(mcp.ServerConfiguration, result_json) or {
assert false, 'Failed to decode result: ${err}'
return
}
// Verify the protocol version matches what was requested
assert result.protocol_version == '2024-11-05', 'Protocol version should match the request'
// Verify server info
assert result.server_info.name == 'developer', 'Server name should be "developer"'
}
// test_tools_list tests the tools/list handler to verify tool registration
fn test_tools_list() {
// Create a new MCP server
mut server := new_mcp_server() or {
assert false, 'Failed to create MCP server: ${err}'
return
}
// Sample tools/list request
tools_list_request := '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"cursor":""}}'
// Process the request through the handler
response := server.handler.handle(tools_list_request) or {
assert false, 'Handler failed to process request: ${err}'
return
}
// Decode the response to verify its structure
decoded_response := jsonrpc.decode_response(response) or {
assert false, 'Failed to decode response: ${err}'
return
}
// Verify that the response is not an error
assert !decoded_response.is_error(), 'Response should not be an error'
// Parse the result to verify its contents
result_json := decoded_response.result() or {
assert false, 'Failed to get result: ${err}'
return
}
// Decode the result into a map to check the tools
result_map := json2.raw_decode(result_json) or {
assert false, 'Failed to decode result: ${err}'
return
}.as_map()
// Verify that the tools array exists and contains the expected tool
tools := result_map['tools'].arr()
assert tools.len > 0, 'Tools list should not be empty'
// Find the generate_module_from_openapi tool
mut found_tool := false
for tool in tools {
tool_map := tool.as_map()
if tool_map['name'].str() == 'generate_module_from_openapi' {
found_tool = true
break
}
}
assert found_tool, 'generate_module_from_openapi tool should be registered'
}

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

@@ -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
}

View File

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

View File

@@ -14,37 +14,37 @@ pub:
config ServerConfiguration
}
// // new_server creates a new MCP server
// pub fn new_server(backend Backend, params ServerParams) !&Server {
// mut server := &Server{
// ServerConfiguration: params.config,
// backend: backend,
// }
// new_server creates a new MCP server
pub fn new_server(backend Backend, params ServerParams) !&Server {
mut server := &Server{
ServerConfiguration: params.config,
backend: backend,
}
// // Create a handler with the core MCP procedures registered
// handler := jsonrpc.new_handler(jsonrpc.Handler{
// procedures: {
// ...params.handlers,
// // Core handlers
// 'initialize': server.initialize_handler,
// 'notifications/initialized': initialized_notification_handler,
// Create a handler with the core MCP procedures registered
handler := jsonrpc.new_handler(jsonrpc.Handler{
procedures: {
...params.handlers,
// Core handlers
'initialize': server.initialize_handler,
'notifications/initialized': initialized_notification_handler,
// // Resource handlers
// 'resources/list': server.resources_list_handler,
// 'resources/read': server.resources_read_handler,
// 'resources/templates/list': server.resources_templates_list_handler,
// 'resources/subscribe': server.resources_subscribe_handler,
// Resource handlers
'resources/list': server.resources_list_handler,
'resources/read': server.resources_read_handler,
'resources/templates/list': server.resources_templates_list_handler,
'resources/subscribe': server.resources_subscribe_handler,
// // Prompt handlers
// 'prompts/list': server.prompts_list_handler,
// 'prompts/get': server.prompts_get_handler,
// Prompt handlers
'prompts/list': server.prompts_list_handler,
'prompts/get': server.prompts_get_handler,
// // Tool handlers
// 'tools/list': server.tools_list_handler,
// 'tools/call': server.tools_call_handler
// }
// })!
// Tool handlers
'tools/list': server.tools_list_handler,
'tools/call': server.tools_call_handler
}
})!
// server.handler = *handler
// return server
// }
server.handler = *handler
return server
}

View File

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

View File

@@ -6,6 +6,7 @@ import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.schemas.jsonschema
import freeflowuniverse.herolib.mcp.logger
// Tool related structs
@@ -14,14 +15,7 @@ pub struct Tool {
pub:
name string
description string
input_schema ToolInputSchema @[json: 'inputSchema']
}
pub struct ToolInputSchema {
pub:
typ string @[json: 'type']
properties map[string]ToolProperty
required []string
input_schema jsonschema.Schema @[json: 'inputSchema']
}
pub struct ToolProperty {
@@ -35,6 +29,7 @@ pub struct ToolItems {
pub:
typ string @[json: 'type']
enum []string
properties map[string]ToolProperty
}
pub struct ToolContent {
@@ -69,12 +64,15 @@ fn (mut s Server) tools_list_handler(data string) !string {
// TODO: Implement pagination logic using the cursor
// For now, return all tools
// Create a success response with the result
response := jsonrpc.new_response_generic[ToolListResult](request.id, ToolListResult{
encoded := json.encode(ToolListResult{
tools: s.backend.tool_list()!
next_cursor: '' // Empty if no more pages
})
// Create a success response with the result
response := jsonrpc.new_response(request.id, json.encode(ToolListResult{
tools: s.backend.tool_list()!
next_cursor: '' // Empty if no more pages
}))
return response.encode()
}
@@ -96,10 +94,8 @@ pub:
// tools_call_handler handles the tools/call request
// This request is used to call a specific tool with arguments
fn (mut s Server) tools_call_handler(data string) !string {
println('debugzo301')
// Decode the request with name and arguments parameters
request_map := json2.raw_decode(data)!.as_map()
println('debugzo30')
params_map := request_map['params'].as_map()
tool_name := params_map['name'].str()
if !s.backend.tool_exists(tool_name)! {
@@ -118,9 +114,11 @@ fn (mut s Server) tools_call_handler(data string) !string {
}
}
log.error('Calling tool: ${tool_name} with arguments: ${arguments}')
// Call the tool with the provided arguments
result := s.backend.tool_call(tool_name, arguments)!
log.error('Received result from tool: ${tool_name} with result: ${result}')
// Create a success response with the result
response := jsonrpc.new_response_generic[ToolCallResult](request_map['id'].int(),
result)
@@ -142,3 +140,13 @@ pub fn (mut s Server) send_tools_list_changed_notification() ! {
// Send the notification to all connected clients
log.info('Sending tools list changed notification: ${json.encode(notification)}')
}
pub fn error_tool_call_result(err IError) ToolCallResult {
return ToolCallResult{
is_error: true
content: [ToolContent{
typ: 'text'
text: err.msg()
}]
}
}

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

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

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

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -1,24 +1,28 @@
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'
}
}

View File

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

View File

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

View File

@@ -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: ''