reorganize and sort mcps
This commit is contained in:
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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')
|
|
||||||
// }
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
// }
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
TOSORT/developer/testdata/mock_module/mock.v
vendored
38
TOSORT/developer/testdata/mock_module/mock.v
vendored
@@ -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}'
|
|
||||||
}
|
|
||||||
159
TOSORT/developer/testdata/vlang_test_standalone.v
vendored
159
TOSORT/developer/testdata/vlang_test_standalone.v
vendored
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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}'
|
|
||||||
@@ -4,54 +4,70 @@ This module provides a V language implementation of the [Model Context Protocol
|
|||||||
|
|
||||||
## Overview
|
## 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
|
## Key Components
|
||||||
|
|
||||||
- **Server**: The main MCP server struct that handles JSON-RPC requests and responses
|
- **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
|
- **Model Configuration**: Structures representing client and server capabilities according to the MCP specification
|
||||||
- **Handlers**: Implementation of MCP protocol handlers, including initialization
|
- **Protocol Handlers**: Implementation of MCP protocol handlers for resources, prompts, tools, and initialization
|
||||||
- **Factory**: Functions to create and configure an MCP server
|
- **Factory**: Functions to create and configure an MCP server with custom backends and handlers
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Full implementation of the MCP protocol version 2024-11-05
|
- Complete implementation of the MCP protocol version 2024-11-05
|
||||||
- JSON-RPC based communication
|
- 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
|
- 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
|
- Logging capabilities
|
||||||
- Resource management
|
- Minimal implementation requirements for server developers
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To create a new MCP server:
|
To create a new MCP server using the core module:
|
||||||
|
|
||||||
```v
|
```v
|
||||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
|
||||||
import freeflowuniverse.herolib.mcp
|
import freeflowuniverse.herolib.mcp
|
||||||
|
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||||
|
|
||||||
// Define custom handlers if needed
|
// Create a backend (memory-based or custom implementation)
|
||||||
handlers := {
|
backend := mcp.MemoryBackend{
|
||||||
'custom_method': my_custom_handler
|
tools: {
|
||||||
|
'my_tool': my_tool_definition
|
||||||
|
}
|
||||||
|
tool_handlers: {
|
||||||
|
'my_tool': my_tool_handler
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create server configuration
|
// Create and configure the server
|
||||||
config := mcp.ServerConfiguration{
|
mut server := mcp.new_server(backend, mcp.ServerParams{
|
||||||
// Configure server capabilities as needed
|
config: mcp.ServerConfiguration{
|
||||||
}
|
server_info: mcp.ServerInfo{
|
||||||
|
name: 'my_mcp_server'
|
||||||
|
version: '1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})!
|
||||||
|
|
||||||
// Create and start the server
|
// Start the server
|
||||||
mut server := mcp.new_server(handlers, config)!
|
|
||||||
server.start()!
|
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
|
- **baobab**: An MCP server implementation for Baobab-specific tools and functionality
|
||||||
- **run**: Execute V files
|
- **developer**: An MCP server implementation focused on developer tools
|
||||||
- **compile**: Compile V files
|
|
||||||
- **vet**: Perform static analysis on V files
|
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
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub mut:
|
|||||||
tool_handlers map[string]ToolHandler
|
tool_handlers map[string]ToolHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type ToolHandler = fn (arguments map[string]json2.Any) !ToolCallResult
|
||||||
fn (b &MemoryBackend) resource_exists(uri string) !bool {
|
fn (b &MemoryBackend) resource_exists(uri string) !bool {
|
||||||
return uri in b.resources
|
return uri in b.resources
|
||||||
}
|
}
|
||||||
@@ -130,7 +131,7 @@ fn (b &MemoryBackend) tool_call(name string, arguments map[string]json2.Any) !To
|
|||||||
content: [
|
content: [
|
||||||
ToolContent{
|
ToolContent{
|
||||||
typ: 'text'
|
typ: 'text'
|
||||||
text: 'Error: ${err.msg}'
|
text: 'Error: ${err.msg()}'
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
3
lib/mcp/baobab/README.md
Normal file
3
lib/mcp/baobab/README.md
Normal 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.
|
||||||
@@ -1,40 +1,101 @@
|
|||||||
module baobab
|
module baobab
|
||||||
|
|
||||||
import freeflowuniverse.herolib.mcp
|
import freeflowuniverse.herolib.mcp
|
||||||
|
import freeflowuniverse.herolib.schemas.jsonschema
|
||||||
|
import freeflowuniverse.herolib.core.code
|
||||||
import x.json2 as json { Any }
|
import x.json2 as json { Any }
|
||||||
import freeflowuniverse.herolib.mcp.logger
|
|
||||||
import freeflowuniverse.herolib.baobab.generator
|
import freeflowuniverse.herolib.baobab.generator
|
||||||
|
import freeflowuniverse.herolib.baobab.specification
|
||||||
|
|
||||||
const generate_module_from_openapi_tool = mcp.Tool{
|
// generate_methods_file MCP Tool
|
||||||
name: 'generate_module_from_openapi'
|
//
|
||||||
description: ''
|
|
||||||
input_schema: mcp.ToolInputSchema{
|
const generate_methods_file_tool = mcp.Tool{
|
||||||
typ: 'object'
|
name: 'generate_methods_file'
|
||||||
properties: {
|
description: 'Generates a methods file with methods for a backend corresponding to thos specified in an OpenAPI or OpenRPC specification'
|
||||||
'openapi_path': mcp.ToolProperty{
|
input_schema: jsonschema.Schema{
|
||||||
typ: 'string'
|
typ: 'object'
|
||||||
items: mcp.ToolItems{
|
properties: {'source': jsonschema.SchemaRef(jsonschema.Schema{
|
||||||
typ: ''
|
typ: 'object'
|
||||||
enum: []
|
properties: {
|
||||||
}
|
'openapi_path': jsonschema.SchemaRef(jsonschema.Schema{
|
||||||
enum: []
|
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 {
|
pub fn (d &Baobab) generate_methods_file_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
|
||||||
println('debugzo31')
|
source := json.decode[generator.Source](arguments["source"].str())!
|
||||||
openapi_path := arguments['openapi_path'].str()
|
result := generator.generate_methods_file_str(source)
|
||||||
println('debugzo32')
|
or {
|
||||||
result := generator.generate_module_from_openapi(openapi_path) or {
|
|
||||||
println('debugzo33')
|
|
||||||
return mcp.error_tool_call_result(err)
|
return mcp.error_tool_call_result(err)
|
||||||
}
|
}
|
||||||
println('debugzo34')
|
|
||||||
return mcp.ToolCallResult{
|
return mcp.ToolCallResult{
|
||||||
is_error: false
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
101
lib/mcp/baobab/baobab_tools_test.v
Normal file
101
lib/mcp/baobab/baobab_tools_test.v
Normal 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
23
lib/mcp/baobab/command.v
Normal 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()!
|
||||||
|
}
|
||||||
@@ -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
128
lib/mcp/baobab/mcp_test.v
Normal 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
34
lib/mcp/baobab/server.v
Normal 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
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,37 +14,37 @@ pub:
|
|||||||
config ServerConfiguration
|
config ServerConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
// // new_server creates a new MCP server
|
// new_server creates a new MCP server
|
||||||
// pub fn new_server(backend Backend, params ServerParams) !&Server {
|
pub fn new_server(backend Backend, params ServerParams) !&Server {
|
||||||
// mut server := &Server{
|
mut server := &Server{
|
||||||
// ServerConfiguration: params.config,
|
ServerConfiguration: params.config,
|
||||||
// backend: backend,
|
backend: backend,
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Create a handler with the core MCP procedures registered
|
// Create a handler with the core MCP procedures registered
|
||||||
// handler := jsonrpc.new_handler(jsonrpc.Handler{
|
handler := jsonrpc.new_handler(jsonrpc.Handler{
|
||||||
// procedures: {
|
procedures: {
|
||||||
// ...params.handlers,
|
...params.handlers,
|
||||||
// // Core handlers
|
// Core handlers
|
||||||
// 'initialize': server.initialize_handler,
|
'initialize': server.initialize_handler,
|
||||||
// 'notifications/initialized': initialized_notification_handler,
|
'notifications/initialized': initialized_notification_handler,
|
||||||
|
|
||||||
// // Resource handlers
|
// Resource handlers
|
||||||
// 'resources/list': server.resources_list_handler,
|
'resources/list': server.resources_list_handler,
|
||||||
// 'resources/read': server.resources_read_handler,
|
'resources/read': server.resources_read_handler,
|
||||||
// 'resources/templates/list': server.resources_templates_list_handler,
|
'resources/templates/list': server.resources_templates_list_handler,
|
||||||
// 'resources/subscribe': server.resources_subscribe_handler,
|
'resources/subscribe': server.resources_subscribe_handler,
|
||||||
|
|
||||||
// // Prompt handlers
|
// Prompt handlers
|
||||||
// 'prompts/list': server.prompts_list_handler,
|
'prompts/list': server.prompts_list_handler,
|
||||||
// 'prompts/get': server.prompts_get_handler,
|
'prompts/get': server.prompts_get_handler,
|
||||||
|
|
||||||
// // Tool handlers
|
// Tool handlers
|
||||||
// 'tools/list': server.tools_list_handler,
|
'tools/list': server.tools_list_handler,
|
||||||
// 'tools/call': server.tools_call_handler
|
'tools/call': server.tools_call_handler
|
||||||
// }
|
}
|
||||||
// })!
|
})!
|
||||||
|
|
||||||
// server.handler = *handler
|
server.handler = *handler
|
||||||
// return server
|
return server
|
||||||
// }
|
}
|
||||||
|
|||||||
@@ -1 +1,53 @@
|
|||||||
module mcp
|
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
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import log
|
|||||||
import x.json2
|
import x.json2
|
||||||
import json
|
import json
|
||||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||||
|
import freeflowuniverse.herolib.schemas.jsonschema
|
||||||
import freeflowuniverse.herolib.mcp.logger
|
import freeflowuniverse.herolib.mcp.logger
|
||||||
|
|
||||||
// Tool related structs
|
// Tool related structs
|
||||||
@@ -14,14 +15,7 @@ pub struct Tool {
|
|||||||
pub:
|
pub:
|
||||||
name string
|
name string
|
||||||
description string
|
description string
|
||||||
input_schema ToolInputSchema @[json: 'inputSchema']
|
input_schema jsonschema.Schema @[json: 'inputSchema']
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ToolInputSchema {
|
|
||||||
pub:
|
|
||||||
typ string @[json: 'type']
|
|
||||||
properties map[string]ToolProperty
|
|
||||||
required []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ToolProperty {
|
pub struct ToolProperty {
|
||||||
@@ -35,6 +29,7 @@ pub struct ToolItems {
|
|||||||
pub:
|
pub:
|
||||||
typ string @[json: 'type']
|
typ string @[json: 'type']
|
||||||
enum []string
|
enum []string
|
||||||
|
properties map[string]ToolProperty
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ToolContent {
|
pub struct ToolContent {
|
||||||
@@ -69,12 +64,15 @@ fn (mut s Server) tools_list_handler(data string) !string {
|
|||||||
|
|
||||||
// TODO: Implement pagination logic using the cursor
|
// TODO: Implement pagination logic using the cursor
|
||||||
// For now, return all tools
|
// For now, return all tools
|
||||||
|
encoded := json.encode(ToolListResult{
|
||||||
// Create a success response with the result
|
|
||||||
response := jsonrpc.new_response_generic[ToolListResult](request.id, ToolListResult{
|
|
||||||
tools: s.backend.tool_list()!
|
tools: s.backend.tool_list()!
|
||||||
next_cursor: '' // Empty if no more pages
|
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()
|
return response.encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,10 +94,8 @@ pub:
|
|||||||
// tools_call_handler handles the tools/call request
|
// tools_call_handler handles the tools/call request
|
||||||
// This request is used to call a specific tool with arguments
|
// This request is used to call a specific tool with arguments
|
||||||
fn (mut s Server) tools_call_handler(data string) !string {
|
fn (mut s Server) tools_call_handler(data string) !string {
|
||||||
println('debugzo301')
|
|
||||||
// Decode the request with name and arguments parameters
|
// Decode the request with name and arguments parameters
|
||||||
request_map := json2.raw_decode(data)!.as_map()
|
request_map := json2.raw_decode(data)!.as_map()
|
||||||
println('debugzo30')
|
|
||||||
params_map := request_map['params'].as_map()
|
params_map := request_map['params'].as_map()
|
||||||
tool_name := params_map['name'].str()
|
tool_name := params_map['name'].str()
|
||||||
if !s.backend.tool_exists(tool_name)! {
|
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
|
// Call the tool with the provided arguments
|
||||||
result := s.backend.tool_call(tool_name, 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
|
// Create a success response with the result
|
||||||
response := jsonrpc.new_response_generic[ToolCallResult](request_map['id'].int(),
|
response := jsonrpc.new_response_generic[ToolCallResult](request_map['id'].int(),
|
||||||
result)
|
result)
|
||||||
@@ -142,3 +140,13 @@ pub fn (mut s Server) send_tools_list_changed_notification() ! {
|
|||||||
// Send the notification to all connected clients
|
// Send the notification to all connected clients
|
||||||
log.info('Sending tools list changed notification: ${json.encode(notification)}')
|
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
92
lib/mcp/mcpgen/README.md
Normal 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
23
lib/mcp/mcpgen/command.v
Normal 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
283
lib/mcp/mcpgen/mcpgen.v
Normal 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
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
module developer
|
module mcpgen
|
||||||
|
|
||||||
import freeflowuniverse.herolib.mcp
|
import freeflowuniverse.herolib.mcp
|
||||||
import x.json2
|
|
||||||
|
|
||||||
pub fn result_to_mcp_tool_contents[T](result T) []mcp.ToolContent {
|
pub fn result_to_mcp_tool_contents[T](result T) []mcp.ToolContent {
|
||||||
return [result_to_mcp_tool_content(result)]
|
return [result_to_mcp_tool_content(result)]
|
||||||
145
lib/mcp/mcpgen/mcpgen_tools.v
Normal file
145
lib/mcp/mcpgen/mcpgen_tools.v
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/mcp/mcpgen/schemas/create_mcp_tools_code_tool_input.json
Normal file
21
lib/mcp/mcpgen/schemas/create_mcp_tools_code_tool_input.json
Normal 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
35
lib/mcp/mcpgen/server.v
Normal 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
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// @{tool_name} MCP Tool
|
// @{tool_name} MCP Tool
|
||||||
|
// @{tool.description}
|
||||||
|
|
||||||
const @{tool_name}_tool = @{tool.str()}
|
const @{tool_name}_tool = @{tool.str()}
|
||||||
|
|
||||||
11
lib/mcp/mcpgen/templates/tool_handler.v.template
Normal file
11
lib/mcp/mcpgen/templates/tool_handler.v.template
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/mcp/mcpgen/templates/tools_file.v.template
Normal file
1
lib/mcp/mcpgen/templates/tools_file.v.template
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@for import in
|
||||||
@@ -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
15
lib/mcp/vcode/command.v
Normal 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()!
|
||||||
|
}
|
||||||
@@ -1,27 +1,31 @@
|
|||||||
module baobab
|
module vcode
|
||||||
|
|
||||||
import freeflowuniverse.herolib.mcp
|
import freeflowuniverse.herolib.mcp
|
||||||
import freeflowuniverse.herolib.mcp.logger
|
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')
|
logger.info('Creating new Developer MCP server')
|
||||||
|
|
||||||
// Initialize the server with the empty handlers map
|
// Initialize the server with the empty handlers map
|
||||||
mut server := mcp.new_server(mcp.MemoryBackend{
|
mut server := mcp.new_server(mcp.MemoryBackend{
|
||||||
tools: {
|
tools: {
|
||||||
'generate_module_from_openapi': generate_module_from_openapi_tool
|
'get_function_from_file': get_function_from_file_tool
|
||||||
}
|
}
|
||||||
tool_handlers: {
|
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{
|
}, mcp.ServerParams{
|
||||||
config: mcp.ServerConfiguration{
|
config: mcp.ServerConfiguration{
|
||||||
server_info: mcp.ServerInfo{
|
server_info: mcp.ServerInfo{
|
||||||
name: 'developer'
|
name: 'vcode'
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})!
|
})!
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module main
|
module vcode
|
||||||
|
|
||||||
import freeflowuniverse.herolib.mcp.logger
|
import freeflowuniverse.herolib.mcp.logger
|
||||||
import freeflowuniverse.herolib.mcp
|
import freeflowuniverse.herolib.mcp
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module developer
|
module vcode
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module developer
|
module vcode
|
||||||
|
|
||||||
import freeflowuniverse.herolib.mcp
|
import freeflowuniverse.herolib.mcp
|
||||||
|
|
||||||
@@ -9,10 +9,10 @@ ARGS:
|
|||||||
file_path string - path to the V file
|
file_path string - path to the V file
|
||||||
function_name string - name of the function to extract
|
function_name string - name of the function to extract
|
||||||
RETURNS: string - the function block including comments, or empty string if not found'
|
RETURNS: string - the function block including comments, or empty string if not found'
|
||||||
input_schema: mcp.ToolInputSchema{
|
input_schema: jsonschema.Schema{
|
||||||
typ: 'object'
|
typ: 'object'
|
||||||
properties: {
|
properties: {
|
||||||
'file_path': mcp.ToolProperty{
|
'file_path': jsonschema.Schema{
|
||||||
typ: 'string'
|
typ: 'string'
|
||||||
items: mcp.ToolItems{
|
items: mcp.ToolItems{
|
||||||
typ: ''
|
typ: ''
|
||||||
@@ -20,7 +20,7 @@ RETURNS: string - the function block including comments, or empty string if not
|
|||||||
}
|
}
|
||||||
enum: []
|
enum: []
|
||||||
}
|
}
|
||||||
'function_name': mcp.ToolProperty{
|
'function_name': jsonschema.Schema{
|
||||||
typ: 'string'
|
typ: 'string'
|
||||||
items: mcp.ToolItems{
|
items: mcp.ToolItems{
|
||||||
typ: ''
|
typ: ''
|
||||||
Reference in New Issue
Block a user