...
This commit is contained in:
68
lib/ai/mcpcore/README.md
Normal file
68
lib/ai/mcpcore/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Model Context Protocol (MCP) Implementation
|
||||
|
||||
This module provides a V language implementation of the [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/specification/2024-11-05/) specification. MCP is a protocol designed to standardize communication between AI models and their context providers.
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
## to test
|
||||
|
||||
```
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
source /root/.bashrc
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
- **Server**: The main MCP server struct that handles JSON-RPC requests and responses
|
||||
- **Backend Interface**: Abstraction for different backend implementations (memory-based by default)
|
||||
- **Model Configuration**: Structures representing client and server capabilities according to the MCP specification
|
||||
- **Protocol Handlers**: Implementation of MCP protocol handlers for resources, prompts, tools, and initialization
|
||||
- **Factory**: Functions to create and configure an MCP server with custom backends and handlers
|
||||
|
||||
## Features
|
||||
|
||||
- Complete implementation of the MCP protocol version 2024-11-05
|
||||
- Handles all boilerplate protocol methods (resources/list, tools/list, prompts/list, etc.)
|
||||
- JSON-RPC based communication layer with automatic request/response handling
|
||||
- Support for client-server capability negotiation
|
||||
- Pluggable backend system for different storage and processing needs
|
||||
- Generic type conversion utilities for MCP tool content
|
||||
- Comprehensive error handling
|
||||
- Logging capabilities
|
||||
- Minimal implementation requirements for server developers
|
||||
|
||||
## Usage
|
||||
|
||||
To create a new MCP server using the core module:
|
||||
|
||||
```v
|
||||
import freeflowuniverse.herolib.ai.mcp
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// Create a backend (memory-based or custom implementation)
|
||||
backend := mcp.MemoryBackend{
|
||||
tools: {
|
||||
'my_tool': my_tool_definition
|
||||
}
|
||||
tool_handlers: {
|
||||
'my_tool': my_tool_handler
|
||||
}
|
||||
}
|
||||
|
||||
// Create and configure the server
|
||||
mut server := mcp.new_server(backend, mcp.ServerParams{
|
||||
config: mcp.ServerConfiguration{
|
||||
server_info: mcp.ServerInfo{
|
||||
name: 'my_mcp_server'
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
})!
|
||||
|
||||
// Start the server
|
||||
server.start()!
|
||||
```
|
||||
3
lib/ai/mcpcore/README2.md
Normal file
3
lib/ai/mcpcore/README2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
If logic is implemented in mcp module, than structure with folders logic and mcp, where logic residers in /logic and mcp related code (like tool and prompt handlers and server code) in /mcp
|
||||
32
lib/ai/mcpcore/backend_interface.v
Normal file
32
lib/ai/mcpcore/backend_interface.v
Normal file
@@ -0,0 +1,32 @@
|
||||
module mcp
|
||||
|
||||
import x.json2
|
||||
|
||||
interface Backend {
|
||||
// Resource methods
|
||||
resource_exists(uri string) !bool
|
||||
resource_get(uri string) !Resource
|
||||
resource_list() ![]Resource
|
||||
resource_subscribed(uri string) !bool
|
||||
resource_contents_get(uri string) ![]ResourceContent
|
||||
resource_templates_list() ![]ResourceTemplate
|
||||
|
||||
// Prompt methods
|
||||
prompt_exists(name string) !bool
|
||||
prompt_get(name string) !Prompt
|
||||
prompt_call(name string, arguments []string) ![]PromptMessage
|
||||
prompt_list() ![]Prompt
|
||||
prompt_messages_get(name string, arguments map[string]string) ![]PromptMessage
|
||||
|
||||
// Tool methods
|
||||
tool_exists(name string) !bool
|
||||
tool_get(name string) !Tool
|
||||
tool_list() ![]Tool
|
||||
tool_call(name string, arguments map[string]json2.Any) !ToolCallResult
|
||||
|
||||
// Sampling methods
|
||||
sampling_create_message(params map[string]json2.Any) !SamplingCreateMessageResult
|
||||
mut:
|
||||
resource_subscribe(uri string) !
|
||||
resource_unsubscribe(uri string) !
|
||||
}
|
||||
183
lib/ai/mcpcore/backend_memory.v
Normal file
183
lib/ai/mcpcore/backend_memory.v
Normal file
@@ -0,0 +1,183 @@
|
||||
module mcpcore
|
||||
|
||||
import x.json2
|
||||
|
||||
pub struct MemoryBackend {
|
||||
pub mut:
|
||||
// Resource related fields
|
||||
resources map[string]Resource
|
||||
subscriptions []string // list of subscribed resource uri's
|
||||
resource_contents map[string][]ResourceContent
|
||||
resource_templates map[string]ResourceTemplate
|
||||
|
||||
// Prompt related fields
|
||||
prompts map[string]Prompt
|
||||
prompt_messages map[string][]PromptMessage
|
||||
prompt_handlers map[string]PromptHandler
|
||||
|
||||
// Tool related fields
|
||||
tools map[string]Tool
|
||||
tool_handlers map[string]ToolHandler
|
||||
|
||||
// Sampling related fields
|
||||
sampling_handler SamplingHandler
|
||||
}
|
||||
|
||||
pub type ToolHandler = fn (arguments map[string]json2.Any) !ToolCallResult
|
||||
|
||||
pub type PromptHandler = fn (arguments []string) ![]PromptMessage
|
||||
|
||||
pub type SamplingHandler = fn (params map[string]json2.Any) !SamplingCreateMessageResult
|
||||
|
||||
fn (b &MemoryBackend) resource_exists(uri string) !bool {
|
||||
return uri in b.resources
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) resource_get(uri string) !Resource {
|
||||
return b.resources[uri] or { return error('resource not found') }
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) resource_list() ![]Resource {
|
||||
return b.resources.values()
|
||||
}
|
||||
|
||||
fn (mut b MemoryBackend) resource_subscribe(uri string) ! {
|
||||
if uri !in b.subscriptions {
|
||||
b.subscriptions << uri
|
||||
}
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) resource_subscribed(uri string) !bool {
|
||||
return uri in b.subscriptions
|
||||
}
|
||||
|
||||
fn (mut b MemoryBackend) resource_unsubscribe(uri string) ! {
|
||||
b.subscriptions = b.subscriptions.filter(it != uri)
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) resource_contents_get(uri string) ![]ResourceContent {
|
||||
return b.resource_contents[uri] or { return error('resource contents not found') }
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) resource_templates_list() ![]ResourceTemplate {
|
||||
return b.resource_templates.values()
|
||||
}
|
||||
|
||||
// Prompt related methods
|
||||
|
||||
fn (b &MemoryBackend) prompt_exists(name string) !bool {
|
||||
return name in b.prompts
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) prompt_get(name string) !Prompt {
|
||||
return b.prompts[name] or { return error('prompt not found') }
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) prompt_list() ![]Prompt {
|
||||
return b.prompts.values()
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) prompt_messages_get(name string, arguments map[string]string) ![]PromptMessage {
|
||||
// Get the base messages for this prompt
|
||||
base_messages := b.prompt_messages[name] or { return error('prompt messages not found') }
|
||||
|
||||
// Apply arguments to the messages
|
||||
mut messages := []PromptMessage{}
|
||||
|
||||
for msg in base_messages {
|
||||
mut content := msg.content
|
||||
|
||||
// If the content is text, replace argument placeholders
|
||||
if content.typ == 'text' {
|
||||
mut text := content.text
|
||||
|
||||
// Replace each argument in the text
|
||||
for arg_name, arg_value in arguments {
|
||||
text = text.replace('{{${arg_name}}}', arg_value)
|
||||
}
|
||||
|
||||
content = PromptContent{
|
||||
typ: content.typ
|
||||
text: text
|
||||
data: content.data
|
||||
mimetype: content.mimetype
|
||||
resource: content.resource
|
||||
}
|
||||
}
|
||||
|
||||
messages << PromptMessage{
|
||||
role: msg.role
|
||||
content: content
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) prompt_call(name string, arguments []string) ![]PromptMessage {
|
||||
// Get the tool handler
|
||||
handler := b.prompt_handlers[name] or { return error('tool handler not found') }
|
||||
|
||||
// Call the handler with the provided arguments
|
||||
return handler(arguments) or { panic(err) }
|
||||
}
|
||||
|
||||
// Tool related methods
|
||||
|
||||
fn (b &MemoryBackend) tool_exists(name string) !bool {
|
||||
return name in b.tools
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) tool_get(name string) !Tool {
|
||||
return b.tools[name] or { return error('tool not found') }
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) tool_list() ![]Tool {
|
||||
return b.tools.values()
|
||||
}
|
||||
|
||||
fn (b &MemoryBackend) tool_call(name string, arguments map[string]json2.Any) !ToolCallResult {
|
||||
// Get the tool handler
|
||||
handler := b.tool_handlers[name] or { return error('tool handler not found') }
|
||||
|
||||
// Call the handler with the provided arguments
|
||||
return handler(arguments) or {
|
||||
// If the handler throws an error, return it as a tool error
|
||||
return ToolCallResult{
|
||||
is_error: true
|
||||
content: [
|
||||
ToolContent{
|
||||
typ: 'text'
|
||||
text: 'Error: ${err.msg()}'
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sampling related methods
|
||||
|
||||
fn (b &MemoryBackend) sampling_create_message(params map[string]json2.Any) !SamplingCreateMessageResult {
|
||||
// Check if a sampling handler is registered
|
||||
if isnil(b.sampling_handler) {
|
||||
// Return a default implementation that just echoes back a message
|
||||
// indicating that no sampling handler is registered
|
||||
return SamplingCreateMessageResult{
|
||||
model: 'default'
|
||||
stop_reason: 'endTurn'
|
||||
role: 'assistant'
|
||||
content: MessageContent{
|
||||
typ: 'text'
|
||||
text: 'Sampling is not configured on this server. Please register a sampling handler.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the sampling handler with the provided parameters
|
||||
return b.sampling_handler(params)!
|
||||
}
|
||||
|
||||
// register_sampling_handler registers a handler for sampling requests
|
||||
pub fn (mut b MemoryBackend) register_sampling_handler(handler SamplingHandler) {
|
||||
b.sampling_handler = handler
|
||||
}
|
||||
68
lib/ai/mcpcore/cmd/compile.vsh
Executable file
68
lib/ai/mcpcore/cmd/compile.vsh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env -S v -n -cg -w -parallel-cc -enable-globals run
|
||||
|
||||
import os
|
||||
import flag
|
||||
|
||||
mut fp := flag.new_flag_parser(os.args)
|
||||
fp.application('compile.vsh')
|
||||
fp.version('v0.1.0')
|
||||
fp.description('Compile MCP binary in debug or production mode')
|
||||
fp.skip_executable()
|
||||
|
||||
prod_mode := fp.bool('prod', `p`, false, 'Build production version (optimized)')
|
||||
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 additional_args.len > 0 {
|
||||
eprintln('Unexpected arguments: ${additional_args.join(' ')}')
|
||||
println(fp.usage())
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Change to the mcp directory
|
||||
mcp_dir := os.dir(os.real_path(os.executable()))
|
||||
os.chdir(mcp_dir) or { panic('Failed to change directory to ${mcp_dir}: ${err}') }
|
||||
|
||||
// Set MCPPATH based on OS
|
||||
mut mcppath := '/usr/local/bin/mcp'
|
||||
if os.user_os() == 'macos' {
|
||||
mcppath = os.join_path(os.home_dir(), 'hero/bin/mcp')
|
||||
}
|
||||
|
||||
// Set compilation command based on OS and mode
|
||||
compile_cmd := if prod_mode {
|
||||
'v -enable-globals -w -n -prod mcp.v'
|
||||
} else {
|
||||
'v -w -cg -gc none -cc tcc -d use_openssl -enable-globals mcp.v'
|
||||
}
|
||||
|
||||
println('Building MCP in ${if prod_mode { 'production' } else { 'debug' }} mode...')
|
||||
|
||||
if os.system(compile_cmd) != 0 {
|
||||
panic('Failed to compile mcp.v with command: ${compile_cmd}')
|
||||
}
|
||||
|
||||
// Make executable
|
||||
os.chmod('mcp', 0o755) or { panic('Failed to make mcp binary executable: ${err}') }
|
||||
|
||||
// Ensure destination directory exists
|
||||
os.mkdir_all(os.dir(mcppath)) or { panic('Failed to create directory ${os.dir(mcppath)}: ${err}') }
|
||||
|
||||
// Copy to destination paths
|
||||
os.cp('mcp', mcppath) or { panic('Failed to copy mcp binary to ${mcppath}: ${err}') }
|
||||
os.cp('mcp', '/tmp/mcp') or { panic('Failed to copy mcp binary to /tmp/mcp: ${err}') }
|
||||
|
||||
// Clean up
|
||||
os.rm('mcp') or { panic('Failed to remove temporary mcp binary: ${err}') }
|
||||
|
||||
println('**MCP COMPILE OK**')
|
||||
93
lib/ai/mcpcore/cmd/mcp.v
Normal file
93
lib/ai/mcpcore/cmd/mcp.v
Normal file
@@ -0,0 +1,93 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import cli { Command, Flag }
|
||||
import freeflowuniverse.herolib.osal.core as osal
|
||||
// import freeflowuniverse.herolib.ai.mcp.vcode
|
||||
// import freeflowuniverse.herolib.ai.mcp.mcpgen
|
||||
// import freeflowuniverse.herolib.ai.mcp.baobab
|
||||
import freeflowuniverse.herolib.ai.mcp.rhai.mcp as rhai_mcp
|
||||
import freeflowuniverse.herolib.ai.mcp.rust
|
||||
|
||||
fn main() {
|
||||
do() or { panic(err) }
|
||||
}
|
||||
|
||||
pub fn do() ! {
|
||||
mut cmd_mcp := Command{
|
||||
name: 'mcp'
|
||||
usage: '
|
||||
## Manage your MCPs
|
||||
|
||||
example:
|
||||
|
||||
mcp
|
||||
'
|
||||
description: 'create, edit, show mdbooks'
|
||||
required_args: 0
|
||||
}
|
||||
|
||||
// cmd_run_add_flags(mut cmd_publisher)
|
||||
|
||||
cmd_mcp.add_flag(Flag{
|
||||
flag: .bool
|
||||
required: false
|
||||
name: 'debug'
|
||||
abbrev: 'd'
|
||||
description: 'show debug output'
|
||||
})
|
||||
|
||||
cmd_mcp.add_flag(Flag{
|
||||
flag: .bool
|
||||
required: false
|
||||
name: 'verbose'
|
||||
abbrev: 'v'
|
||||
description: 'show verbose output'
|
||||
})
|
||||
|
||||
mut cmd_inspector := Command{
|
||||
sort_flags: true
|
||||
name: 'inspector'
|
||||
execute: cmd_inspector_execute
|
||||
description: 'will list existing mdbooks'
|
||||
}
|
||||
|
||||
cmd_inspector.add_flag(Flag{
|
||||
flag: .string
|
||||
required: false
|
||||
name: 'name'
|
||||
abbrev: 'n'
|
||||
description: 'name of the MCP'
|
||||
})
|
||||
|
||||
cmd_inspector.add_flag(Flag{
|
||||
flag: .bool
|
||||
required: false
|
||||
name: 'open'
|
||||
abbrev: 'o'
|
||||
description: 'open inspector'
|
||||
})
|
||||
|
||||
cmd_mcp.add_command(rhai_mcp.command)
|
||||
cmd_mcp.add_command(rust.command)
|
||||
// cmd_mcp.add_command(baobab.command)
|
||||
// cmd_mcp.add_command(vcode.command)
|
||||
cmd_mcp.add_command(cmd_inspector)
|
||||
// cmd_mcp.add_command(vcode.command)
|
||||
cmd_mcp.setup()
|
||||
cmd_mcp.parse(os.args)
|
||||
}
|
||||
|
||||
fn cmd_inspector_execute(cmd Command) ! {
|
||||
open := cmd.flags.get_bool('open') or { false }
|
||||
if open {
|
||||
osal.exec(cmd: 'open http://localhost:5173')!
|
||||
}
|
||||
name := cmd.flags.get_string('name') or { '' }
|
||||
if name.len > 0 {
|
||||
println('starting inspector for MCP ${name}')
|
||||
osal.exec(cmd: 'npx @modelcontextprotocol/inspector mcp ${name} start')!
|
||||
} else {
|
||||
osal.exec(cmd: 'npx @modelcontextprotocol/inspector')!
|
||||
}
|
||||
}
|
||||
49
lib/ai/mcpcore/factory.v
Normal file
49
lib/ai/mcpcore/factory.v
Normal file
@@ -0,0 +1,49 @@
|
||||
module mcp
|
||||
|
||||
import time
|
||||
import os
|
||||
import log
|
||||
import x.json2
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
@[params]
|
||||
pub struct ServerParams {
|
||||
pub:
|
||||
handlers map[string]jsonrpc.ProcedureHandler
|
||||
config ServerConfiguration
|
||||
}
|
||||
|
||||
// new_server creates a new MCP server
|
||||
pub fn new_server(backend Backend, params ServerParams) !&Server {
|
||||
mut server := &Server{
|
||||
ServerConfiguration: params.config
|
||||
backend: backend
|
||||
}
|
||||
|
||||
// Create a handler with the core MCP procedures registered
|
||||
handler := jsonrpc.new_handler(jsonrpc.Handler{
|
||||
procedures: {
|
||||
//...params.handlers,
|
||||
// Core handlers
|
||||
'initialize': server.initialize_handler
|
||||
'notifications/initialized': initialized_notification_handler
|
||||
// Resource handlers
|
||||
'resources/list': server.resources_list_handler
|
||||
'resources/read': server.resources_read_handler
|
||||
'resources/templates/list': server.resources_templates_list_handler
|
||||
'resources/subscribe': server.resources_subscribe_handler
|
||||
// Prompt handlers
|
||||
'prompts/list': server.prompts_list_handler
|
||||
'prompts/get': server.prompts_get_handler
|
||||
'completion/complete': server.prompts_get_handler
|
||||
// Tool handlers
|
||||
'tools/list': server.tools_list_handler
|
||||
'tools/call': server.tools_call_handler
|
||||
// Sampling handlers
|
||||
'sampling/createMessage': server.sampling_create_message_handler
|
||||
}
|
||||
})!
|
||||
|
||||
server.handler = *handler
|
||||
return server
|
||||
}
|
||||
52
lib/ai/mcpcore/generics.v
Normal file
52
lib/ai/mcpcore/generics.v
Normal file
@@ -0,0 +1,52 @@
|
||||
module mcp
|
||||
|
||||
pub fn result_to_mcp_tool_contents[T](result T) []ToolContent {
|
||||
return [result_to_mcp_tool_content[T](result)]
|
||||
}
|
||||
|
||||
pub fn result_to_mcp_tool_content[T](result T) ToolContent {
|
||||
$if T is string {
|
||||
return ToolContent{
|
||||
typ: 'text'
|
||||
text: result.str()
|
||||
}
|
||||
} $else $if T is int {
|
||||
return ToolContent{
|
||||
typ: 'number'
|
||||
number: result.int()
|
||||
}
|
||||
} $else $if T is bool {
|
||||
return 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
|
||||
}
|
||||
27
lib/ai/mcpcore/handler_initialize.v
Normal file
27
lib/ai/mcpcore/handler_initialize.v
Normal file
@@ -0,0 +1,27 @@
|
||||
module mcpcore
|
||||
|
||||
import time
|
||||
import os
|
||||
import log
|
||||
import x.json2
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// initialize_handler handles the initialize request according to the MCP specification
|
||||
fn (mut s Server) initialize_handler(data string) !string {
|
||||
// Decode the request with ClientConfiguration parameters
|
||||
request := jsonrpc.decode_request_generic[ClientConfiguration](data)!
|
||||
s.client_config = request.params
|
||||
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response_generic[ServerConfiguration](request.id, s.ServerConfiguration)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// initialized_notification_handler handles the initialized notification
|
||||
// This notification is sent by the client after successful initialization
|
||||
fn initialized_notification_handler(data string) !string {
|
||||
// This is a notification, so no response is expected
|
||||
// Just log that we received the notification
|
||||
log.info('Received initialized notification')
|
||||
return ''
|
||||
}
|
||||
103
lib/ai/mcpcore/handler_initialize_test.v
Normal file
103
lib/ai/mcpcore/handler_initialize_test.v
Normal file
@@ -0,0 +1,103 @@
|
||||
module mcp
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import json
|
||||
|
||||
// This file contains tests for the MCP initialize handler implementation.
|
||||
// It tests the handler's ability to process initialize requests according to the MCP specification.
|
||||
|
||||
// test_initialize_handler tests the initialize handler with a sample initialize request
|
||||
fn test_initialize_handler() {
|
||||
mut server := Server{}
|
||||
|
||||
// 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"}}}'
|
||||
|
||||
// Call the initialize handler directly
|
||||
response := server.initialize_handler(initialize_request) or {
|
||||
assert false, 'Initialize handler failed: ${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(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 capabilities
|
||||
assert result.capabilities.prompts.list_changed == true, 'Prompts capability should have list_changed set to true'
|
||||
assert result.capabilities.resources.subscribe == true, 'Resources capability should have subscribe set to true'
|
||||
assert result.capabilities.resources.list_changed == true, 'Resources capability should have list_changed set to true'
|
||||
assert result.capabilities.tools.list_changed == true, 'Tools capability should have list_changed set to true'
|
||||
|
||||
// Verify server info
|
||||
assert result.server_info.name == 'HeroLibMCPServer', 'Server name should be HeroLibMCPServer'
|
||||
assert result.server_info.version == '1.0.0', 'Server version should be 1.0.0'
|
||||
}
|
||||
|
||||
// test_initialize_handler_with_handler tests the initialize handler through the JSONRPC handler
|
||||
fn test_initialize_handler_with_handler() {
|
||||
mut server := Server{}
|
||||
|
||||
// Create a handler with just the initialize procedure
|
||||
handler := jsonrpc.new_handler(jsonrpc.Handler{
|
||||
procedures: {
|
||||
'initialize': server.initialize_handler
|
||||
}
|
||||
}) or {
|
||||
assert false, 'Failed to create handler: ${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 := 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(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'
|
||||
}
|
||||
135
lib/ai/mcpcore/handler_prompts.v
Normal file
135
lib/ai/mcpcore/handler_prompts.v
Normal file
@@ -0,0 +1,135 @@
|
||||
module mcp
|
||||
|
||||
import time
|
||||
import os
|
||||
import log
|
||||
import x.json2
|
||||
import json
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// Prompt related structs
|
||||
|
||||
pub struct Prompt {
|
||||
pub:
|
||||
name string
|
||||
description string
|
||||
arguments []PromptArgument
|
||||
}
|
||||
|
||||
pub struct PromptArgument {
|
||||
pub:
|
||||
name string
|
||||
description string
|
||||
required bool
|
||||
}
|
||||
|
||||
pub struct PromptMessage {
|
||||
pub:
|
||||
role string
|
||||
content PromptContent
|
||||
}
|
||||
|
||||
pub struct PromptContent {
|
||||
pub:
|
||||
typ string @[json: 'type']
|
||||
text string
|
||||
data string
|
||||
mimetype string @[json: 'mimeType']
|
||||
resource ResourceContent
|
||||
}
|
||||
|
||||
// Prompt List Handler
|
||||
|
||||
pub struct PromptListParams {
|
||||
pub:
|
||||
cursor string
|
||||
}
|
||||
|
||||
pub struct PromptListResult {
|
||||
pub:
|
||||
prompts []Prompt
|
||||
next_cursor string @[json: 'nextCursor']
|
||||
}
|
||||
|
||||
// prompts_list_handler handles the prompts/list request
|
||||
// This request is used to retrieve a list of available prompts
|
||||
fn (mut s Server) prompts_list_handler(data string) !string {
|
||||
// Decode the request with cursor parameter
|
||||
request := jsonrpc.decode_request_generic[PromptListParams](data)!
|
||||
cursor := request.params.cursor
|
||||
|
||||
// TODO: Implement pagination logic using the cursor
|
||||
// For now, return all prompts
|
||||
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response_generic[PromptListResult](request.id, PromptListResult{
|
||||
prompts: s.backend.prompt_list()!
|
||||
next_cursor: '' // Empty if no more pages
|
||||
})
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Prompt Get Handler
|
||||
|
||||
pub struct PromptGetParams {
|
||||
pub:
|
||||
name string
|
||||
arguments map[string]string
|
||||
}
|
||||
|
||||
pub struct PromptGetResult {
|
||||
pub:
|
||||
description string
|
||||
messages []PromptMessage
|
||||
}
|
||||
|
||||
// prompts_get_handler handles the prompts/get request
|
||||
// This request is used to retrieve a specific prompt with arguments
|
||||
fn (mut s Server) prompts_get_handler(data string) !string {
|
||||
// Decode the request with name and arguments parameters
|
||||
request_map := json2.raw_decode(data)!.as_map()
|
||||
params_map := request_map['params'].as_map()
|
||||
|
||||
if !s.backend.prompt_exists(params_map['name'].str())! {
|
||||
return jsonrpc.new_error_response(request_map['id'].int(), prompt_not_found(params_map['name'].str())).encode()
|
||||
}
|
||||
|
||||
// Get the prompt by name
|
||||
prompt := s.backend.prompt_get(params_map['name'].str())!
|
||||
|
||||
// Validate required arguments
|
||||
for arg in prompt.arguments {
|
||||
if arg.required && params_map['arguments'].as_map()[arg.name].str() == '' {
|
||||
return jsonrpc.new_error_response(request_map['id'].int(), missing_required_argument(arg.name)).encode()
|
||||
}
|
||||
}
|
||||
|
||||
messages := s.backend.prompt_call(params_map['name'].str(), params_map['arguments'].as_map().values().map(it.str()))!
|
||||
|
||||
// // Get the prompt messages with arguments applied
|
||||
// messages := s.backend.prompt_messages_get(request.params.name, request.params.arguments)!
|
||||
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response_generic[PromptGetResult](request_map['id'].int(),
|
||||
PromptGetResult{
|
||||
description: prompt.description
|
||||
messages: messages
|
||||
})
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Prompt Notification Handlers
|
||||
|
||||
// send_prompts_list_changed_notification sends a notification when the list of prompts changes
|
||||
pub fn (mut s Server) send_prompts_list_changed_notification() ! {
|
||||
// Check if the client supports this notification
|
||||
if !s.client_config.capabilities.roots.list_changed {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a notification
|
||||
notification := jsonrpc.new_blank_notification('notifications/prompts/list_changed')
|
||||
s.send(json.encode(notification))
|
||||
// Send the notification to all connected clients
|
||||
log.info('Sending prompts list changed notification: ${json.encode(notification)}')
|
||||
}
|
||||
186
lib/ai/mcpcore/handler_resources.v
Normal file
186
lib/ai/mcpcore/handler_resources.v
Normal file
@@ -0,0 +1,186 @@
|
||||
module mcp
|
||||
|
||||
import time
|
||||
import os
|
||||
import log
|
||||
import x.json2
|
||||
import json
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
pub struct Resource {
|
||||
pub:
|
||||
uri string
|
||||
name string
|
||||
description string
|
||||
mimetype string @[json: 'mimeType']
|
||||
}
|
||||
|
||||
// Resource List Handler
|
||||
|
||||
pub struct ResourceListParams {
|
||||
pub:
|
||||
cursor string
|
||||
}
|
||||
|
||||
pub struct ResourceListResult {
|
||||
pub:
|
||||
resources []Resource
|
||||
next_cursor string @[json: 'nextCursor']
|
||||
}
|
||||
|
||||
// resources_list_handler handles the resources/list request
|
||||
// This request is used to retrieve a list of available resources
|
||||
fn (mut s Server) resources_list_handler(data string) !string {
|
||||
// Decode the request with cursor parameter
|
||||
request := jsonrpc.decode_request_generic[ResourceListParams](data)!
|
||||
cursor := request.params.cursor
|
||||
|
||||
// TODO: Implement pagination logic using the cursor
|
||||
// For now, return all resources
|
||||
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response_generic[ResourceListResult](request.id, ResourceListResult{
|
||||
resources: s.backend.resource_list()!
|
||||
next_cursor: '' // Empty if no more pages
|
||||
})
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Resource Read Handler
|
||||
|
||||
pub struct ResourceReadParams {
|
||||
pub:
|
||||
uri string
|
||||
}
|
||||
|
||||
pub struct ResourceReadResult {
|
||||
pub:
|
||||
contents []ResourceContent
|
||||
}
|
||||
|
||||
pub struct ResourceContent {
|
||||
pub:
|
||||
uri string
|
||||
mimetype string @[json: 'mimeType']
|
||||
text string
|
||||
blob string // Base64-encoded binary data
|
||||
}
|
||||
|
||||
// resources_read_handler handles the resources/read request
|
||||
// This request is used to retrieve the contents of a resource
|
||||
fn (mut s Server) resources_read_handler(data string) !string {
|
||||
// Decode the request with uri parameter
|
||||
request := jsonrpc.decode_request_generic[ResourceReadParams](data)!
|
||||
|
||||
if !s.backend.resource_exists(request.params.uri)! {
|
||||
return jsonrpc.new_error_response(request.id, resource_not_found(request.params.uri)).encode()
|
||||
}
|
||||
|
||||
// Get the resource contents by URI
|
||||
resource_contents := s.backend.resource_contents_get(request.params.uri)!
|
||||
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response_generic[ResourceReadResult](request.id, ResourceReadResult{
|
||||
contents: resource_contents
|
||||
})
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Resource Templates Handler
|
||||
|
||||
pub struct ResourceTemplatesListResult {
|
||||
pub:
|
||||
resource_templates []ResourceTemplate @[json: 'resourceTemplates']
|
||||
}
|
||||
|
||||
pub struct ResourceTemplate {
|
||||
pub:
|
||||
uri_template string @[json: 'uriTemplate']
|
||||
name string
|
||||
description string
|
||||
mimetype string @[json: 'mimeType']
|
||||
}
|
||||
|
||||
// resources_templates_list_handler handles the resources/templates/list request
|
||||
// This request is used to retrieve a list of available resource templates
|
||||
fn (mut s Server) resources_templates_list_handler(data string) !string {
|
||||
// Decode the request
|
||||
request := jsonrpc.decode_request(data)!
|
||||
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response_generic[ResourceTemplatesListResult](request.id,
|
||||
ResourceTemplatesListResult{
|
||||
resource_templates: s.backend.resource_templates_list()!
|
||||
})
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Resource Subscription Handler
|
||||
|
||||
pub struct ResourceSubscribeParams {
|
||||
pub:
|
||||
uri string
|
||||
}
|
||||
|
||||
pub struct ResourceSubscribeResult {
|
||||
pub:
|
||||
subscribed bool
|
||||
}
|
||||
|
||||
// resources_subscribe_handler handles the resources/subscribe request
|
||||
// This request is used to subscribe to changes for a specific resource
|
||||
fn (mut s Server) resources_subscribe_handler(data string) !string {
|
||||
request := jsonrpc.decode_request_generic[ResourceSubscribeParams](data)!
|
||||
|
||||
if !s.backend.resource_exists(request.params.uri)! {
|
||||
return jsonrpc.new_error_response(request.id, resource_not_found(request.params.uri)).encode()
|
||||
}
|
||||
|
||||
s.backend.resource_subscribe(request.params.uri)!
|
||||
|
||||
response := jsonrpc.new_response_generic[ResourceSubscribeResult](request.id, ResourceSubscribeResult{
|
||||
subscribed: true
|
||||
})
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Resource Notification Handlers
|
||||
|
||||
// send_resources_list_changed_notification sends a notification when the list of resources changes
|
||||
pub fn (mut s Server) send_resources_list_changed_notification() ! {
|
||||
// Check if the client supports this notification
|
||||
if !s.client_config.capabilities.roots.list_changed {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a notification
|
||||
notification := jsonrpc.new_blank_notification('notifications/resources/list_changed')
|
||||
s.send(json.encode(notification))
|
||||
// Send the notification to all connected clients
|
||||
// In a real implementation, this would use a WebSocket or other transport
|
||||
log.info('Sending resources list changed notification: ${json.encode(notification)}')
|
||||
}
|
||||
|
||||
pub struct ResourceUpdatedParams {
|
||||
pub:
|
||||
uri string
|
||||
}
|
||||
|
||||
// send_resource_updated_notification sends a notification when a subscribed resource is updated
|
||||
pub fn (mut s Server) send_resource_updated_notification(uri string) ! {
|
||||
// Check if the client is subscribed to this resource
|
||||
if !s.backend.resource_subscribed(uri)! {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a notification
|
||||
notification := jsonrpc.new_notification[ResourceUpdatedParams]('notifications/resources/updated',
|
||||
ResourceUpdatedParams{
|
||||
uri: uri
|
||||
})
|
||||
|
||||
s.send(json.encode(notification))
|
||||
// Send the notification to all connected clients
|
||||
// In a real implementation, this would use a WebSocket or other transport
|
||||
log.info('Sending resource updated notification: ${json.encode(notification)}')
|
||||
}
|
||||
145
lib/ai/mcpcore/handler_sampling.v
Normal file
145
lib/ai/mcpcore/handler_sampling.v
Normal file
@@ -0,0 +1,145 @@
|
||||
module mcp
|
||||
|
||||
import time
|
||||
import os
|
||||
import log
|
||||
import x.json2
|
||||
import json
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// Sampling related structs
|
||||
|
||||
pub struct MessageContent {
|
||||
pub:
|
||||
typ string @[json: 'type']
|
||||
text string
|
||||
data string
|
||||
mimetype string @[json: 'mimeType']
|
||||
}
|
||||
|
||||
pub struct Message {
|
||||
pub:
|
||||
role string
|
||||
content MessageContent
|
||||
}
|
||||
|
||||
pub struct ModelHint {
|
||||
pub:
|
||||
name string
|
||||
}
|
||||
|
||||
pub struct ModelPreferences {
|
||||
pub:
|
||||
hints []ModelHint
|
||||
cost_priority f32 @[json: 'costPriority']
|
||||
speed_priority f32 @[json: 'speedPriority']
|
||||
intelligence_priority f32 @[json: 'intelligencePriority']
|
||||
}
|
||||
|
||||
pub struct SamplingCreateMessageParams {
|
||||
pub:
|
||||
messages []Message
|
||||
model_preferences ModelPreferences @[json: 'modelPreferences']
|
||||
system_prompt string @[json: 'systemPrompt']
|
||||
include_context string @[json: 'includeContext']
|
||||
temperature f32
|
||||
max_tokens int @[json: 'maxTokens']
|
||||
stop_sequences []string @[json: 'stopSequences']
|
||||
metadata map[string]json2.Any
|
||||
}
|
||||
|
||||
pub struct SamplingCreateMessageResult {
|
||||
pub:
|
||||
model string
|
||||
stop_reason string @[json: 'stopReason']
|
||||
role string
|
||||
content MessageContent
|
||||
}
|
||||
|
||||
// sampling_create_message_handler handles the sampling/createMessage request
|
||||
// This request is used to request LLM completions through the client
|
||||
fn (mut s Server) sampling_create_message_handler(data string) !string {
|
||||
// Decode the request
|
||||
request_map := json2.raw_decode(data)!.as_map()
|
||||
id := request_map['id'].int()
|
||||
params_map := request_map['params'].as_map()
|
||||
|
||||
// Validate required parameters
|
||||
if 'messages' !in params_map {
|
||||
return jsonrpc.new_error_response(id, missing_required_argument('messages')).encode()
|
||||
}
|
||||
|
||||
if 'maxTokens' !in params_map {
|
||||
return jsonrpc.new_error_response(id, missing_required_argument('maxTokens')).encode()
|
||||
}
|
||||
|
||||
// Call the backend to handle the sampling request
|
||||
result := s.backend.sampling_create_message(params_map) or {
|
||||
return jsonrpc.new_error_response(id, sampling_error(err.msg())).encode()
|
||||
}
|
||||
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response(id, json.encode(result))
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Helper function to convert JSON messages to our Message struct format
|
||||
fn parse_messages(messages_json json2.Any) ![]Message {
|
||||
messages_arr := messages_json.arr()
|
||||
mut result := []Message{cap: messages_arr.len}
|
||||
|
||||
for msg_json in messages_arr {
|
||||
msg_map := msg_json.as_map()
|
||||
|
||||
if 'role' !in msg_map {
|
||||
return error('Missing role in message')
|
||||
}
|
||||
|
||||
if 'content' !in msg_map {
|
||||
return error('Missing content in message')
|
||||
}
|
||||
|
||||
role := msg_map['role'].str()
|
||||
content_map := msg_map['content'].as_map()
|
||||
|
||||
if 'type' !in content_map {
|
||||
return error('Missing type in message content')
|
||||
}
|
||||
|
||||
typ := content_map['type'].str()
|
||||
mut text := ''
|
||||
mut data := ''
|
||||
mut mimetype := ''
|
||||
|
||||
if typ == 'text' {
|
||||
if 'text' !in content_map {
|
||||
return error('Missing text in text content')
|
||||
}
|
||||
text = content_map['text'].str()
|
||||
} else if typ == 'image' {
|
||||
if 'data' !in content_map {
|
||||
return error('Missing data in image content')
|
||||
}
|
||||
data = content_map['data'].str()
|
||||
|
||||
if 'mimeType' !in content_map {
|
||||
return error('Missing mimeType in image content')
|
||||
}
|
||||
mimetype = content_map['mimeType'].str()
|
||||
} else {
|
||||
return error('Unsupported content type: ${typ}')
|
||||
}
|
||||
|
||||
result << Message{
|
||||
role: role
|
||||
content: MessageContent{
|
||||
typ: typ
|
||||
text: text
|
||||
data: data
|
||||
mimetype: mimetype
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
151
lib/ai/mcpcore/handler_tools.v
Normal file
151
lib/ai/mcpcore/handler_tools.v
Normal file
@@ -0,0 +1,151 @@
|
||||
module mcp
|
||||
|
||||
import time
|
||||
import os
|
||||
import log
|
||||
import x.json2
|
||||
import json
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
|
||||
// Tool related structs
|
||||
|
||||
pub struct Tool {
|
||||
pub:
|
||||
name string
|
||||
description string
|
||||
input_schema jsonschema.Schema @[json: 'inputSchema']
|
||||
}
|
||||
|
||||
pub struct ToolProperty {
|
||||
pub:
|
||||
typ string @[json: 'type']
|
||||
items ToolItems
|
||||
enum []string
|
||||
}
|
||||
|
||||
pub struct ToolItems {
|
||||
pub:
|
||||
typ string @[json: 'type']
|
||||
enum []string
|
||||
properties map[string]ToolProperty
|
||||
}
|
||||
|
||||
pub struct ToolContent {
|
||||
pub:
|
||||
typ string @[json: 'type']
|
||||
text string
|
||||
number int
|
||||
boolean bool
|
||||
properties map[string]ToolContent
|
||||
items []ToolContent
|
||||
}
|
||||
|
||||
// Tool List Handler
|
||||
|
||||
pub struct ToolListParams {
|
||||
pub:
|
||||
cursor string
|
||||
}
|
||||
|
||||
pub struct ToolListResult {
|
||||
pub:
|
||||
tools []Tool
|
||||
next_cursor string @[json: 'nextCursor']
|
||||
}
|
||||
|
||||
// tools_list_handler handles the tools/list request
|
||||
// This request is used to retrieve a list of available tools
|
||||
fn (mut s Server) tools_list_handler(data string) !string {
|
||||
// Decode the request with cursor parameter
|
||||
request := jsonrpc.decode_request_generic[ToolListParams](data)!
|
||||
cursor := request.params.cursor
|
||||
|
||||
// TODO: Implement pagination logic using the cursor
|
||||
// For now, return all tools
|
||||
encoded := json.encode(ToolListResult{
|
||||
tools: s.backend.tool_list()!
|
||||
next_cursor: '' // Empty if no more pages
|
||||
})
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response(request.id, json.encode(ToolListResult{
|
||||
tools: s.backend.tool_list()!
|
||||
next_cursor: '' // Empty if no more pages
|
||||
}))
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Tool Call Handler
|
||||
|
||||
pub struct ToolCallParams {
|
||||
pub:
|
||||
name string
|
||||
arguments map[string]json2.Any
|
||||
meta map[string]json2.Any @[json: '_meta']
|
||||
}
|
||||
|
||||
pub struct ToolCallResult {
|
||||
pub:
|
||||
is_error bool @[json: 'isError']
|
||||
content []ToolContent
|
||||
}
|
||||
|
||||
// tools_call_handler handles the tools/call request
|
||||
// This request is used to call a specific tool with arguments
|
||||
fn (mut s Server) tools_call_handler(data string) !string {
|
||||
// Decode the request with name and arguments parameters
|
||||
request_map := json2.raw_decode(data)!.as_map()
|
||||
params_map := request_map['params'].as_map()
|
||||
tool_name := params_map['name'].str()
|
||||
if !s.backend.tool_exists(tool_name)! {
|
||||
return jsonrpc.new_error_response(request_map['id'].int(), tool_not_found(tool_name)).encode()
|
||||
}
|
||||
|
||||
arguments := params_map['arguments'].as_map()
|
||||
// Get the tool by name
|
||||
tool := s.backend.tool_get(tool_name)!
|
||||
|
||||
// Validate arguments against the input schema
|
||||
// TODO: Implement proper JSON Schema validation
|
||||
for req in tool.input_schema.required {
|
||||
if req !in arguments {
|
||||
return jsonrpc.new_error_response(request_map['id'].int(), missing_required_argument(req)).encode()
|
||||
}
|
||||
}
|
||||
|
||||
log.error('Calling tool: ${tool_name} with arguments: ${arguments}')
|
||||
// Call the tool with the provided arguments
|
||||
result := s.backend.tool_call(tool_name, arguments)!
|
||||
|
||||
log.error('Received result from tool: ${tool_name} with result: ${result}')
|
||||
// Create a success response with the result
|
||||
response := jsonrpc.new_response_generic[ToolCallResult](request_map['id'].int(),
|
||||
result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
// Tool Notification Handlers
|
||||
|
||||
// send_tools_list_changed_notification sends a notification when the list of tools changes
|
||||
pub fn (mut s Server) send_tools_list_changed_notification() ! {
|
||||
// Check if the client supports this notification
|
||||
if !s.client_config.capabilities.roots.list_changed {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a notification
|
||||
notification := jsonrpc.new_blank_notification('notifications/tools/list_changed')
|
||||
s.send(json.encode(notification))
|
||||
// Send the notification to all connected clients
|
||||
log.info('Sending tools list changed notification: ${json.encode(notification)}')
|
||||
}
|
||||
|
||||
pub fn error_tool_call_result(err IError) ToolCallResult {
|
||||
return ToolCallResult{
|
||||
is_error: true
|
||||
content: [ToolContent{
|
||||
typ: 'text'
|
||||
text: err.msg()
|
||||
}]
|
||||
}
|
||||
}
|
||||
93
lib/ai/mcpcore/model_configuration.v
Normal file
93
lib/ai/mcpcore/model_configuration.v
Normal file
@@ -0,0 +1,93 @@
|
||||
module mcp
|
||||
|
||||
import time
|
||||
import os
|
||||
import log
|
||||
import x.json2
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
const protocol_version = '2024-11-05'
|
||||
// MCP server implementation using stdio transport
|
||||
// Based on https://modelcontextprotocol.io/docs/concepts/transports
|
||||
|
||||
// ClientConfiguration represents the parameters for the initialize request
|
||||
pub struct ClientConfiguration {
|
||||
pub:
|
||||
protocol_version string @[json: 'protocolVersion']
|
||||
capabilities ClientCapabilities
|
||||
client_info ClientInfo @[json: 'clientInfo']
|
||||
}
|
||||
|
||||
// ClientCapabilities represents the client capabilities
|
||||
pub struct ClientCapabilities {
|
||||
pub:
|
||||
roots RootsCapability // Ability to provide filesystem roots
|
||||
sampling SamplingCapability // Support for LLM sampling requests
|
||||
experimental ExperimentalCapability // Describes support for non-standard experimental features
|
||||
}
|
||||
|
||||
// RootsCapability represents the roots capability
|
||||
pub struct RootsCapability {
|
||||
pub:
|
||||
list_changed bool @[json: 'listChanged']
|
||||
}
|
||||
|
||||
// SamplingCapability represents the sampling capability
|
||||
pub struct SamplingCapability {}
|
||||
|
||||
// ExperimentalCapability represents the experimental capability
|
||||
pub struct ExperimentalCapability {}
|
||||
|
||||
// ClientInfo represents the client information
|
||||
pub struct ClientInfo {
|
||||
pub:
|
||||
name string
|
||||
version string
|
||||
}
|
||||
|
||||
// ServerConfiguration represents the server configuration
|
||||
pub struct ServerConfiguration {
|
||||
pub:
|
||||
protocol_version string = '2024-11-05' @[json: 'protocolVersion']
|
||||
capabilities ServerCapabilities
|
||||
server_info ServerInfo @[json: 'serverInfo']
|
||||
}
|
||||
|
||||
// ServerCapabilities represents the server capabilities
|
||||
pub struct ServerCapabilities {
|
||||
pub:
|
||||
logging LoggingCapability
|
||||
prompts PromptsCapability
|
||||
resources ResourcesCapability
|
||||
tools ToolsCapability
|
||||
}
|
||||
|
||||
// LoggingCapability represents the logging capability
|
||||
pub struct LoggingCapability {
|
||||
}
|
||||
|
||||
// PromptsCapability represents the prompts capability
|
||||
pub struct PromptsCapability {
|
||||
pub:
|
||||
list_changed bool = true @[json: 'listChanged']
|
||||
}
|
||||
|
||||
// ResourcesCapability represents the resources capability
|
||||
pub struct ResourcesCapability {
|
||||
pub:
|
||||
subscribe bool = true @[json: 'subscribe']
|
||||
list_changed bool = true @[json: 'listChanged']
|
||||
}
|
||||
|
||||
// ToolsCapability represents the tools capability
|
||||
pub struct ToolsCapability {
|
||||
pub:
|
||||
list_changed bool = true @[json: 'listChanged']
|
||||
}
|
||||
|
||||
// ServerInfo represents the server information
|
||||
pub struct ServerInfo {
|
||||
pub:
|
||||
name string = 'HeroLibMCPServer'
|
||||
version string = '1.0.0'
|
||||
}
|
||||
91
lib/ai/mcpcore/model_configuration_test.v
Normal file
91
lib/ai/mcpcore/model_configuration_test.v
Normal file
@@ -0,0 +1,91 @@
|
||||
module mcp
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import json
|
||||
|
||||
// This file contains tests for the MCP initialize handler implementation.
|
||||
// It tests the handler's ability to process initialize requests according to the MCP specification.
|
||||
|
||||
// test_json_serialization_deserialization tests the JSON serialization and deserialization of initialize request and response
|
||||
fn test_json_serialization_deserialization() {
|
||||
// Create a sample initialize params object
|
||||
params := ClientConfiguration{
|
||||
protocol_version: '2024-11-05'
|
||||
capabilities: ClientCapabilities{
|
||||
roots: RootsCapability{
|
||||
list_changed: true
|
||||
}
|
||||
// sampling: SamplingCapability{}
|
||||
}
|
||||
client_info: ClientInfo{
|
||||
name: 'mcp-inspector'
|
||||
// version: '0.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the params to JSON
|
||||
params_json := json.encode(params)
|
||||
|
||||
// Verify the JSON structure has the correct camelCase keys
|
||||
assert params_json.contains('"protocolVersion":"2024-11-05"'), 'JSON should have protocolVersion in camelCase'
|
||||
assert params_json.contains('"clientInfo":{'), 'JSON should have clientInfo in camelCase'
|
||||
assert params_json.contains('"listChanged":true'), 'JSON should have listChanged in camelCase'
|
||||
|
||||
// Deserialize the JSON back to a struct
|
||||
deserialized_params := json.decode(ClientConfiguration, params_json) or {
|
||||
assert false, 'Failed to deserialize params: ${err}'
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the deserialized object matches the original
|
||||
assert deserialized_params.protocol_version == params.protocol_version, 'Deserialized protocol_version should match original'
|
||||
assert deserialized_params.client_info.name == params.client_info.name, 'Deserialized client_info.name should match original'
|
||||
assert deserialized_params.client_info.version == params.client_info.version, 'Deserialized client_info.version should match original'
|
||||
assert deserialized_params.capabilities.roots.list_changed == params.capabilities.roots.list_changed, 'Deserialized capabilities.roots.list_changed should match original'
|
||||
|
||||
// Now test the response serialization/deserialization
|
||||
response := ServerConfiguration{
|
||||
protocol_version: '2024-11-05'
|
||||
capabilities: ServerCapabilities{
|
||||
logging: LoggingCapability{}
|
||||
prompts: PromptsCapability{
|
||||
list_changed: true
|
||||
}
|
||||
resources: ResourcesCapability{
|
||||
subscribe: true
|
||||
list_changed: true
|
||||
}
|
||||
tools: ToolsCapability{
|
||||
list_changed: true
|
||||
}
|
||||
}
|
||||
server_info: ServerInfo{
|
||||
name: 'HeroLibMCPServer'
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the response to JSON
|
||||
response_json := json.encode(response)
|
||||
|
||||
// Verify the JSON structure has the correct camelCase keys
|
||||
assert response_json.contains('"protocolVersion":"2024-11-05"'), 'JSON should have protocolVersion in camelCase'
|
||||
assert response_json.contains('"serverInfo":{'), 'JSON should have serverInfo in camelCase'
|
||||
assert response_json.contains('"listChanged":true'), 'JSON should have listChanged in camelCase'
|
||||
assert response_json.contains('"subscribe":true'), 'JSON should have subscribe field'
|
||||
|
||||
// Deserialize the JSON back to a struct
|
||||
deserialized_response := json.decode(ServerConfiguration, response_json) or {
|
||||
assert false, 'Failed to deserialize response: ${err}'
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the deserialized object matches the original
|
||||
assert deserialized_response.protocol_version == response.protocol_version, 'Deserialized protocol_version should match original'
|
||||
assert deserialized_response.server_info.name == response.server_info.name, 'Deserialized server_info.name should match original'
|
||||
assert deserialized_response.server_info.version == response.server_info.version, 'Deserialized server_info.version should match original'
|
||||
assert deserialized_response.capabilities.prompts.list_changed == response.capabilities.prompts.list_changed, 'Deserialized capabilities.prompts.list_changed should match original'
|
||||
assert deserialized_response.capabilities.resources.subscribe == response.capabilities.resources.subscribe, 'Deserialized capabilities.resources.subscribe should match original'
|
||||
assert deserialized_response.capabilities.resources.list_changed == response.capabilities.resources.list_changed, 'Deserialized capabilities.resources.list_changed should match original'
|
||||
assert deserialized_response.capabilities.tools.list_changed == response.capabilities.tools.list_changed, 'Deserialized capabilities.tools.list_changed should match original'
|
||||
}
|
||||
42
lib/ai/mcpcore/model_error.v
Normal file
42
lib/ai/mcpcore/model_error.v
Normal file
@@ -0,0 +1,42 @@
|
||||
module mcpcore
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// resource_not_found indicates that the requested resource doesn't exist.
|
||||
// This error is returned when the resource specified in the request is not found.
|
||||
// Error code: -32002
|
||||
pub fn resource_not_found(uri string) jsonrpc.RPCError {
|
||||
return jsonrpc.RPCError{
|
||||
code: -32002
|
||||
message: 'Resource not found'
|
||||
data: 'The requested resource ${uri} was not found.'
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_not_found(name string) jsonrpc.RPCError {
|
||||
return jsonrpc.RPCError{
|
||||
code: -32602 // Invalid params
|
||||
message: 'Prompt not found: ${name}'
|
||||
}
|
||||
}
|
||||
|
||||
fn missing_required_argument(arg_name string) jsonrpc.RPCError {
|
||||
return jsonrpc.RPCError{
|
||||
code: -32602 // Invalid params
|
||||
message: 'Missing required argument: ${arg_name}'
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_not_found(name string) jsonrpc.RPCError {
|
||||
return jsonrpc.RPCError{
|
||||
code: -32602 // Invalid params
|
||||
message: 'Tool not found: ${name}'
|
||||
}
|
||||
}
|
||||
|
||||
fn sampling_error(message string) jsonrpc.RPCError {
|
||||
return jsonrpc.RPCError{
|
||||
code: -32603 // Internal error
|
||||
message: 'Sampling error: ${message}'
|
||||
}
|
||||
}
|
||||
56
lib/ai/mcpcore/server.v
Normal file
56
lib/ai/mcpcore/server.v
Normal file
@@ -0,0 +1,56 @@
|
||||
module mcp
|
||||
|
||||
import time
|
||||
import os
|
||||
import log
|
||||
import x.json2
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// Server is the main MCP server struct
|
||||
@[heap]
|
||||
pub struct Server {
|
||||
ServerConfiguration
|
||||
pub mut:
|
||||
client_config ClientConfiguration
|
||||
handler jsonrpc.Handler
|
||||
backend Backend
|
||||
}
|
||||
|
||||
// start starts the MCP server
|
||||
pub fn (mut s Server) start() ! {
|
||||
log.info('Starting MCP server')
|
||||
for {
|
||||
// Read a message from stdin
|
||||
message := os.get_line()
|
||||
if message == '' {
|
||||
time.sleep(10000) // prevent cpu spinning
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle the message using the JSON-RPC handler
|
||||
response := s.handler.handle(message) or {
|
||||
log.error('message: ${message}')
|
||||
log.error('Error handling message: ${err}')
|
||||
|
||||
// Try to extract the request ID
|
||||
id := jsonrpc.decode_request_id(message) or { 0 }
|
||||
|
||||
// Create an internal error response
|
||||
error_response := jsonrpc.new_error(id, jsonrpc.internal_error).encode()
|
||||
print(error_response)
|
||||
continue
|
||||
}
|
||||
|
||||
// Send the response only if it's not empty (notifications return empty responses)
|
||||
if response.len > 0 {
|
||||
s.send(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send sends a response to the client
|
||||
pub fn (mut s Server) send(response string) {
|
||||
// Send the response
|
||||
println(response)
|
||||
flush_stdout()
|
||||
}
|
||||
Reference in New Issue
Block a user