mcp refactor wip
This commit is contained in:
92
lib/mcp/vcode/README.md
Normal file
92
lib/mcp/vcode/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# V-Do MCP Server
|
||||
|
||||
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}
|
||||
```
|
||||
2
lib/mcp/vcode/cmd/.gitignore
vendored
Normal file
2
lib/mcp/vcode/cmd/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
main
|
||||
|
||||
16
lib/mcp/vcode/cmd/compile.sh
Executable file
16
lib/mcp/vcode/cmd/compile.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
export name="mcp_vcode"
|
||||
|
||||
# Change to the directory containing this script
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Compile the V program
|
||||
v -n -w -gc none -cc tcc -d use_openssl -enable-globals main.v
|
||||
|
||||
# Ensure the binary is executable
|
||||
chmod +x main
|
||||
mv main ~/hero/bin/${name}
|
||||
|
||||
echo "Compilation successful. Binary '${name}' is ready."
|
||||
17
lib/mcp/vcode/cmd/main.v
Normal file
17
lib/mcp/vcode/cmd/main.v
Normal file
@@ -0,0 +1,17 @@
|
||||
module main
|
||||
|
||||
import freeflowuniverse.herolib.mcp.vcode
|
||||
|
||||
fn main() {
|
||||
// Create a new MCP server
|
||||
mut server := vcode.new_mcp_server() or {
|
||||
eprintln('Failed to create MCP server: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
// Start the server
|
||||
server.start() or {
|
||||
eprintln('Failed to start MCP server: ${err}')
|
||||
return
|
||||
}
|
||||
}
|
||||
33
lib/mcp/vcode/logic/server.v
Normal file
33
lib/mcp/vcode/logic/server.v
Normal file
@@ -0,0 +1,33 @@
|
||||
module vcode
|
||||
|
||||
import freeflowuniverse.herolib.mcp
|
||||
import freeflowuniverse.herolib.mcp.logger
|
||||
|
||||
@[heap]
|
||||
pub struct VCode {
|
||||
v_version string = '0.1.0'
|
||||
}
|
||||
|
||||
pub fn new_mcp_server(v &VCode) !&mcp.Server {
|
||||
logger.info('Creating new Developer MCP server')
|
||||
|
||||
// Initialize the server with the empty handlers map
|
||||
mut server := mcp.new_server(mcp.MemoryBackend{
|
||||
tools: {
|
||||
'get_function_from_file': get_function_from_file_tool
|
||||
'write_vfile': write_vfile_tool
|
||||
}
|
||||
tool_handlers: {
|
||||
'get_function_from_file': v.get_function_from_file_tool_handler
|
||||
'write_vfile': v.write_vfile_tool_handler
|
||||
}
|
||||
}, mcp.ServerParams{
|
||||
config: mcp.ServerConfiguration{
|
||||
server_info: mcp.ServerInfo{
|
||||
name: 'vcode'
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
})!
|
||||
return server
|
||||
}
|
||||
105
lib/mcp/vcode/logic/test_client.vsh
Normal file
105
lib/mcp/vcode/logic/test_client.vsh
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import os
|
||||
import flag
|
||||
import json
|
||||
|
||||
// Simple test client for the V-Do MCP server
|
||||
// This script sends test requests to the MCP server and displays the responses
|
||||
|
||||
// struct MCPRequest {
|
||||
// id string
|
||||
// method string
|
||||
// params map[string]string
|
||||
// jsonrpc string = '2.0'
|
||||
// }
|
||||
|
||||
// fn send_request(method string, fullpath string) {
|
||||
// // Create the request
|
||||
// request := MCPRequest{
|
||||
// id: '1'
|
||||
// method: method
|
||||
// params: {
|
||||
// 'fullpath': fullpath
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Encode to JSON
|
||||
// json_str := json.encode(request)
|
||||
|
||||
// // Format the message with headers
|
||||
// message := 'Content-Length: ${json_str.len}\r\n\r\n${json_str}'
|
||||
|
||||
// // Write to a temporary file
|
||||
// os.write_file('/tmp/mcp_request.txt', message) or {
|
||||
// eprintln('Failed to write request to file: $err')
|
||||
// return
|
||||
// }
|
||||
|
||||
// // Execute the MCP server with the request
|
||||
// cmd := 'cat /tmp/mcp_request.txt | v run /Users/despiegk/code/github/freeflowuniverse/herolib/lib/mcp/v_do/main.v'
|
||||
// result := os.execute(cmd)
|
||||
|
||||
// if result.exit_code != 0 {
|
||||
// eprintln('Error executing MCP server: ${result.output}')
|
||||
// return
|
||||
// }
|
||||
|
||||
// // Parse and display the response
|
||||
// response := result.output
|
||||
// println('Raw response:')
|
||||
// println('-----------------------------------')
|
||||
// println(response)
|
||||
// println('-----------------------------------')
|
||||
|
||||
// // Try to extract the JSON part
|
||||
// if response.contains('{') && response.contains('}') {
|
||||
// json_start := response.index_after('{', 0)
|
||||
// json_end := response.last_index_of('}')
|
||||
// if json_start >= 0 && json_end >= 0 && json_end > json_start {
|
||||
// json_part := response[json_start-1..json_end+1]
|
||||
// println('Extracted JSON:')
|
||||
// println(json_part)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Parse command line arguments
|
||||
// mut fp := flag.new_flag_parser(os.args)
|
||||
// fp.application('test_client.vsh')
|
||||
// fp.version('v0.1.0')
|
||||
// fp.description('Test client for V-Do MCP server')
|
||||
// fp.skip_executable()
|
||||
|
||||
// method := fp.string('method', `m`, 'test', 'Method to call (test, run, compile, vet)')
|
||||
// fullpath := fp.string('path', `p`, '', 'Path to the file or directory to process')
|
||||
// help_requested := fp.bool('help', `h`, false, 'Show help message')
|
||||
|
||||
// if help_requested {
|
||||
// println(fp.usage())
|
||||
// exit(0)
|
||||
// }
|
||||
|
||||
// additional_args := fp.finalize() or {
|
||||
// eprintln(err)
|
||||
// println(fp.usage())
|
||||
// exit(1)
|
||||
// }
|
||||
|
||||
// if fullpath == '' {
|
||||
// eprintln('Error: Path is required')
|
||||
// println(fp.usage())
|
||||
// exit(1)
|
||||
// }
|
||||
|
||||
// // Validate method
|
||||
// valid_methods := ['test', 'run', 'compile', 'vet']
|
||||
// if method !in valid_methods {
|
||||
// eprintln('Error: Invalid method. Must be one of: ${valid_methods}')
|
||||
// println(fp.usage())
|
||||
// exit(1)
|
||||
// }
|
||||
|
||||
// // Send the request
|
||||
// println('Sending $method request for $fullpath...')
|
||||
// send_request(method, fullpath)
|
||||
284
lib/mcp/vcode/logic/vlang.v
Normal file
284
lib/mcp/vcode/logic/vlang.v
Normal file
@@ -0,0 +1,284 @@
|
||||
module vcode
|
||||
|
||||
import freeflowuniverse.herolib.mcp
|
||||
import freeflowuniverse.herolib.mcp.logger
|
||||
import os
|
||||
import log
|
||||
|
||||
fn get_module_dir(mod string) string {
|
||||
module_parts := mod.trim_string_left('freeflowuniverse.herolib').split('.')
|
||||
return '${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/${module_parts.join('/')}'
|
||||
}
|
||||
|
||||
// given a module path and a type name, returns the type definition of that type within that module
|
||||
// for instance: get_type_from_module('lib/mcp/developer/vlang.v', 'Developer') might return struct Developer {...}
|
||||
fn get_type_from_module(module_path string, type_name string) !string {
|
||||
println('Looking for type ${type_name} in module ${module_path}')
|
||||
v_files := list_v_files(module_path) or {
|
||||
return error('Failed to list V files in ${module_path}: ${err}')
|
||||
}
|
||||
|
||||
for v_file in v_files {
|
||||
println('Checking file: ${v_file}')
|
||||
content := os.read_file(v_file) or { return error('Failed to read file ${v_file}: ${err}') }
|
||||
|
||||
// Look for both regular and pub struct declarations
|
||||
mut type_str := 'struct ${type_name} {'
|
||||
mut i := content.index(type_str) or { -1 }
|
||||
mut is_pub := false
|
||||
|
||||
if i == -1 {
|
||||
// Try with pub struct
|
||||
type_str = 'pub struct ${type_name} {'
|
||||
i = content.index(type_str) or { -1 }
|
||||
is_pub = true
|
||||
}
|
||||
|
||||
if i == -1 {
|
||||
type_import := content.split_into_lines().filter(it.contains('import')
|
||||
&& it.contains(type_name))
|
||||
if type_import.len > 0 {
|
||||
log.debug('debugzoooo')
|
||||
mod := type_import[0].trim_space().trim_string_left('import ').all_before(' ')
|
||||
return get_type_from_module(get_module_dir(mod), type_name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
println('Found type ${type_name} in ${v_file} at position ${i}')
|
||||
|
||||
// Find the start of the struct definition including comments
|
||||
mut comment_start := i
|
||||
mut line_start := i
|
||||
|
||||
// Find the start of the line containing the struct definition
|
||||
for j := i; j >= 0; j-- {
|
||||
if j == 0 || content[j - 1] == `\n` {
|
||||
line_start = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find the start of the comment block (if any)
|
||||
for j := line_start - 1; j >= 0; j-- {
|
||||
if j == 0 {
|
||||
comment_start = 0
|
||||
break
|
||||
}
|
||||
|
||||
// If we hit a blank line or a non-comment line, stop
|
||||
if content[j] == `\n` {
|
||||
if j > 0 && j < content.len - 1 {
|
||||
// Check if the next line starts with a comment
|
||||
next_line_start := j + 1
|
||||
if next_line_start < content.len && content[next_line_start] != `/` {
|
||||
comment_start = j + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the end of the struct definition
|
||||
closing_i := find_closing_brace(content, i + type_str.len) or {
|
||||
return error('could not find where declaration for type ${type_name} ends')
|
||||
}
|
||||
|
||||
// Get the full struct definition including the struct declaration line
|
||||
full_struct := content.substr(line_start, closing_i + 1)
|
||||
println('Found struct definition:\n${full_struct}')
|
||||
|
||||
// Return the full struct definition
|
||||
return full_struct
|
||||
}
|
||||
|
||||
return error('type ${type_name} not found in module ${module_path}')
|
||||
}
|
||||
|
||||
// given a module path and a function name, returns the function definition of that function within that module
|
||||
// for instance: get_function_from_module('lib/mcp/developer/vlang.v', 'develop') might return fn develop(...) {...}
|
||||
fn get_function_from_module(module_path string, function_name string) !string {
|
||||
v_files := list_v_files(module_path) or {
|
||||
return error('Failed to list V files in ${module_path}: ${err}')
|
||||
}
|
||||
|
||||
println('Found ${v_files.len} V files in ${module_path}')
|
||||
for v_file in v_files {
|
||||
println('Checking file: ${v_file}')
|
||||
result := get_function_from_file(v_file, function_name) or {
|
||||
println('Function not found in ${v_file}: ${err}')
|
||||
continue
|
||||
}
|
||||
println('Found function ${function_name} in ${v_file}')
|
||||
return result
|
||||
}
|
||||
|
||||
return error('function ${function_name} not found in module ${module_path}')
|
||||
}
|
||||
|
||||
fn find_closing_brace(content string, start_i int) ?int {
|
||||
mut brace_count := 1
|
||||
for i := start_i; i < content.len; i++ {
|
||||
if content[i] == `{` {
|
||||
brace_count++
|
||||
} else if content[i] == `}` {
|
||||
brace_count--
|
||||
if brace_count == 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// get_function_from_file parses a V file and extracts a specific function block including its comments
|
||||
// ARGS:
|
||||
// file_path string - path to the V file
|
||||
// function_name string - name of the function to extract
|
||||
// RETURNS: string - the function block including comments, or empty string if not found
|
||||
fn get_function_from_file(file_path string, function_name string) !string {
|
||||
content := os.read_file(file_path) or {
|
||||
return error('Failed to read file: ${file_path}: ${err}')
|
||||
}
|
||||
|
||||
lines := content.split_into_lines()
|
||||
mut result := []string{}
|
||||
mut in_function := false
|
||||
mut brace_count := 0
|
||||
mut comment_block := []string{}
|
||||
|
||||
for i, line in lines {
|
||||
trimmed := line.trim_space()
|
||||
|
||||
// Collect comments that might be above the function
|
||||
if trimmed.starts_with('//') {
|
||||
if !in_function {
|
||||
comment_block << line
|
||||
} else if brace_count > 0 {
|
||||
result << line
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we found the function
|
||||
if !in_function && (trimmed.starts_with('fn ${function_name}(')
|
||||
|| trimmed.starts_with('pub fn ${function_name}(')) {
|
||||
in_function = true
|
||||
// Add collected comments
|
||||
result << comment_block
|
||||
comment_block = []
|
||||
result << line
|
||||
if line.contains('{') {
|
||||
brace_count++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// If we're inside the function, keep track of braces
|
||||
if in_function {
|
||||
result << line
|
||||
|
||||
for c in line {
|
||||
if c == `{` {
|
||||
brace_count++
|
||||
} else if c == `}` {
|
||||
brace_count--
|
||||
}
|
||||
}
|
||||
|
||||
// If brace_count is 0, we've reached the end of the function
|
||||
if brace_count == 0 && trimmed.contains('}') {
|
||||
return result.join('\n')
|
||||
}
|
||||
} else {
|
||||
// Reset comment block if we pass a blank line
|
||||
if trimmed == '' {
|
||||
comment_block = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !in_function {
|
||||
return error('Function "${function_name}" not found in ${file_path}')
|
||||
}
|
||||
|
||||
return result.join('\n')
|
||||
}
|
||||
|
||||
// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v
|
||||
fn list_v_files(dir string) ![]string {
|
||||
files := os.ls(dir) or { return error('Error listing directory: ${err}') }
|
||||
|
||||
mut v_files := []string{}
|
||||
for file in files {
|
||||
if file.ends_with('.v') && !file.ends_with('_.v') {
|
||||
filepath := os.join_path(dir, file)
|
||||
v_files << filepath
|
||||
}
|
||||
}
|
||||
|
||||
return v_files
|
||||
}
|
||||
|
||||
// test runs v test on the specified file or directory
|
||||
pub fn vtest(fullpath string) !string {
|
||||
logger.info('test ${fullpath}')
|
||||
if !os.exists(fullpath) {
|
||||
return error('File or directory does not exist: ${fullpath}')
|
||||
}
|
||||
if os.is_dir(fullpath) {
|
||||
mut results := ''
|
||||
for item in list_v_files(fullpath)! {
|
||||
results += vtest(item)!
|
||||
results += '\n-----------------------\n'
|
||||
}
|
||||
return results
|
||||
} else {
|
||||
cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}'
|
||||
logger.debug('Executing command: ${cmd}')
|
||||
result := os.execute(cmd)
|
||||
if result.exit_code != 0 {
|
||||
return error('Test failed for ${fullpath} with exit code ${result.exit_code}\n${result.output}')
|
||||
} else {
|
||||
logger.info('Test completed for ${fullpath}')
|
||||
}
|
||||
return 'Command: ${cmd}\nExit code: ${result.exit_code}\nOutput:\n${result.output}'
|
||||
}
|
||||
}
|
||||
|
||||
// vvet runs v vet on the specified file or directory
|
||||
pub fn vvet(fullpath string) !string {
|
||||
logger.info('vet ${fullpath}')
|
||||
if !os.exists(fullpath) {
|
||||
return error('File or directory does not exist: ${fullpath}')
|
||||
}
|
||||
|
||||
if os.is_dir(fullpath) {
|
||||
mut results := ''
|
||||
files := list_v_files(fullpath) or { return error('Error listing V files: ${err}') }
|
||||
for file in files {
|
||||
results += vet_file(file) or {
|
||||
logger.error('Failed to vet ${file}: ${err}')
|
||||
return error('Failed to vet ${file}: ${err}')
|
||||
}
|
||||
results += '\n-----------------------\n'
|
||||
}
|
||||
return results
|
||||
} else {
|
||||
return vet_file(fullpath)
|
||||
}
|
||||
}
|
||||
|
||||
// vet_file runs v vet on a single file
|
||||
fn vet_file(file string) !string {
|
||||
cmd := 'v vet -v -w ${file}'
|
||||
logger.debug('Executing command: ${cmd}')
|
||||
result := os.execute(cmd)
|
||||
if result.exit_code != 0 {
|
||||
return error('Vet failed for ${file} with exit code ${result.exit_code}\n${result.output}')
|
||||
} else {
|
||||
logger.info('Vet completed for ${file}')
|
||||
}
|
||||
return 'Command: ${cmd}\nExit code: ${result.exit_code}\nOutput:\n${result.output}'
|
||||
}
|
||||
|
||||
// cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath}'
|
||||
100
lib/mcp/vcode/logic/vlang_test.v
Normal file
100
lib/mcp/vcode/logic/vlang_test.v
Normal file
@@ -0,0 +1,100 @@
|
||||
module vcode
|
||||
|
||||
import os
|
||||
|
||||
// Test file for the get_type_from_module function in vlang.v
|
||||
|
||||
// This test verifies that the get_type_from_module function correctly extracts
|
||||
// struct definitions from V source 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!')
|
||||
}
|
||||
39
lib/mcp/vcode/logic/vlang_tools.v
Normal file
39
lib/mcp/vcode/logic/vlang_tools.v
Normal file
@@ -0,0 +1,39 @@
|
||||
module vcode
|
||||
|
||||
import freeflowuniverse.herolib.mcp
|
||||
import freeflowuniverse.herolib.core.code
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
import x.json2 {Any}
|
||||
|
||||
const get_function_from_file_tool = mcp.Tool{
|
||||
name: 'get_function_from_file'
|
||||
description: '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'
|
||||
input_schema: jsonschema.Schema{
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'file_path': jsonschema.SchemaRef(jsonschema.Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'function_name': jsonschema.SchemaRef(jsonschema.Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
required: ['file_path', 'function_name']
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (d &VCode) get_function_from_file_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
|
||||
file_path := arguments['file_path'].str()
|
||||
function_name := arguments['function_name'].str()
|
||||
result := code.get_function_from_file(file_path, function_name) or {
|
||||
return mcp.error_tool_call_result(err)
|
||||
}
|
||||
return mcp.ToolCallResult{
|
||||
is_error: false
|
||||
content: mcp.result_to_mcp_tool_contents[string](result.vgen())
|
||||
}
|
||||
}
|
||||
71
lib/mcp/vcode/logic/write_vfile_tool.v
Normal file
71
lib/mcp/vcode/logic/write_vfile_tool.v
Normal file
@@ -0,0 +1,71 @@
|
||||
module vcode
|
||||
|
||||
import freeflowuniverse.herolib.mcp
|
||||
import freeflowuniverse.herolib.core.code
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
import x.json2 {Any}
|
||||
|
||||
const write_vfile_tool = mcp.Tool{
|
||||
name: 'write_vfile'
|
||||
description: 'write_vfile parses a V code string into a VFile and writes it to the specified path
|
||||
ARGS:
|
||||
path string - directory path where to write the file
|
||||
code string - V code content to write
|
||||
format bool - whether to format the code (optional, default: false)
|
||||
overwrite bool - whether to overwrite existing file (optional, default: false)
|
||||
prefix string - prefix to add to the filename (optional, default: "")
|
||||
RETURNS: string - success message with the path of the written file'
|
||||
input_schema: jsonschema.Schema{
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'path': jsonschema.SchemaRef(jsonschema.Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'code': jsonschema.SchemaRef(jsonschema.Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'format': jsonschema.SchemaRef(jsonschema.Schema{
|
||||
typ: 'boolean'
|
||||
})
|
||||
'overwrite': jsonschema.SchemaRef(jsonschema.Schema{
|
||||
typ: 'boolean'
|
||||
})
|
||||
'prefix': jsonschema.SchemaRef(jsonschema.Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
required: ['path', 'code']
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (d &VCode) write_vfile_tool_handler(arguments map[string]Any) !mcp.ToolCallResult {
|
||||
path := arguments['path'].str()
|
||||
code_str := arguments['code'].str()
|
||||
|
||||
// Parse optional parameters with defaults
|
||||
format := if 'format' in arguments { arguments['format'].bool() } else { false }
|
||||
overwrite := if 'overwrite' in arguments { arguments['overwrite'].bool() } else { false }
|
||||
prefix := if 'prefix' in arguments { arguments['prefix'].str() } else { '' }
|
||||
|
||||
// Create write options
|
||||
options := code.WriteOptions{
|
||||
format: format
|
||||
overwrite: overwrite
|
||||
prefix: prefix
|
||||
}
|
||||
|
||||
// Parse the V code string into a VFile
|
||||
vfile := code.parse_vfile(code_str) or {
|
||||
return mcp.error_tool_call_result(err)
|
||||
}
|
||||
|
||||
// Write the VFile to the specified path
|
||||
vfile.write(path, options) or {
|
||||
return mcp.error_tool_call_result(err)
|
||||
}
|
||||
|
||||
return mcp.ToolCallResult{
|
||||
is_error: false
|
||||
content: mcp.result_to_mcp_tool_contents[string]('Successfully wrote V file to ${path}')
|
||||
}
|
||||
}
|
||||
54
lib/mcp/vcode/mcp/handlers.v
Normal file
54
lib/mcp/vcode/mcp/handlers.v
Normal file
@@ -0,0 +1,54 @@
|
||||
module pugconvert
|
||||
|
||||
import freeflowuniverse.herolib.mcp
|
||||
import x.json2 as json { Any }
|
||||
import freeflowuniverse.herolib.mcp.aitools.pugconvert
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
pub fn handler(arguments map[string]Any) !mcp.ToolCallResult {
|
||||
path := arguments['path'].str()
|
||||
|
||||
// Check if path exists
|
||||
if !os.exists(path) {
|
||||
return mcp.ToolCallResult{
|
||||
is_error: true
|
||||
content: mcp.result_to_mcp_tool_contents[string]("Error: Path '${path}' does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if path is a file or directory
|
||||
is_directory := os.is_dir(path)
|
||||
|
||||
mut message := ""
|
||||
|
||||
if is_directory {
|
||||
// Convert all pug files in the directory
|
||||
pugconvert.convert_pug(path) or {
|
||||
return mcp.ToolCallResult{
|
||||
is_error: true
|
||||
content: mcp.result_to_mcp_tool_contents[string]("Error converting pug files in directory: ${err}")
|
||||
}
|
||||
}
|
||||
message = "Successfully converted all pug files in directory '${path}'"
|
||||
} else if path.ends_with(".v") {
|
||||
// Convert a single pug file
|
||||
pugconvert.convert_pug_file(path) or {
|
||||
return mcp.ToolCallResult{
|
||||
is_error: true
|
||||
content: mcp.result_to_mcp_tool_contents[string]("Error converting pug file: ${err}")
|
||||
}
|
||||
}
|
||||
message = "Successfully converted pug file '${path}'"
|
||||
} else {
|
||||
return mcp.ToolCallResult{
|
||||
is_error: true
|
||||
content: mcp.result_to_mcp_tool_contents[string]("Error: Path '${path}' is not a directory or .pug file")
|
||||
}
|
||||
}
|
||||
|
||||
return mcp.ToolCallResult{
|
||||
is_error: false
|
||||
content: mcp.result_to_mcp_tool_contents[string](message)
|
||||
}
|
||||
}
|
||||
27
lib/mcp/vcode/mcp/mcp.v
Normal file
27
lib/mcp/vcode/mcp/mcp.v
Normal file
@@ -0,0 +1,27 @@
|
||||
module pugconvert
|
||||
|
||||
import freeflowuniverse.herolib.mcp
|
||||
import freeflowuniverse.herolib.mcp.logger
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
pub fn new_mcp_server() !&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: {
|
||||
'pugconvert': specs
|
||||
}
|
||||
tool_handlers: {
|
||||
'pugconvert': handler
|
||||
}
|
||||
}, mcp.ServerParams{
|
||||
config: mcp.ServerConfiguration{
|
||||
server_info: mcp.ServerInfo{
|
||||
name: 'developer'
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
})!
|
||||
return server
|
||||
}
|
||||
21
lib/mcp/vcode/mcp/specifications.v
Normal file
21
lib/mcp/vcode/mcp/specifications.v
Normal file
@@ -0,0 +1,21 @@
|
||||
module pugconvert
|
||||
|
||||
import freeflowuniverse.herolib.mcp
|
||||
import x.json2 as json { Any }
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
import freeflowuniverse.herolib.mcp.logger
|
||||
|
||||
const specs = mcp.Tool{
|
||||
name: 'pugconvert'
|
||||
description: 'Convert Pug template files to Jet template files'
|
||||
input_schema: jsonschema.Schema{
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'path': jsonschema.SchemaRef(jsonschema.Schema{
|
||||
typ: 'string',
|
||||
description: 'Path to a .pug file or directory containing .pug files to convert'
|
||||
})
|
||||
}
|
||||
required: ['path']
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user