implement tools resources and prompts for mcp

This commit is contained in:
Timur Gordon
2025-03-14 23:05:55 +01:00
parent 475e812ba3
commit 8b9b0678b8
12 changed files with 748 additions and 7 deletions

View File

@@ -94,6 +94,11 @@ pub fn path_fix_absolute(path string) string {
return "/${path_fix(path)}"
}
// normalize a file path while preserving path structure
pub fn path_fix(path string) string {
return path.trim('/')
}
// remove underscores and extension
pub fn name_fix_no_ext(name_ string) string {

View File

@@ -0,0 +1,28 @@
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_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]string) !ToolCallResult
mut:
resource_subscribe(uri string) !
resource_unsubscribe(uri string) !
}

138
lib/mcp/backend_memory.v Normal file
View File

@@ -0,0 +1,138 @@
module mcp
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
// Tool related fields
tools map[string]Tool
tool_handlers map[string]ToolHandler
}
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
}
// 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]string) !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}'
}
]
}
}
}

View File

@@ -7,18 +7,41 @@ import x.json2
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.mcp.logger
@[params]
pub struct ServerParams {
pub:
handlers map[string]jsonrpc.ProcedureHandler
config ServerConfiguration
}
// new_server creates a new MCP server
pub fn new_server(handlers map[string]jsonrpc.ProcedureHandler, config ServerConfiguration) !&Server {
pub fn new_server(backend Backend, params ServerParams) !&Server {
mut server := &Server{
ServerConfiguration: config
ServerConfiguration: params.config,
backend: backend,
}
// Create a handler with the core MCP procedures registered
handler := jsonrpc.new_handler(jsonrpc.Handler{
procedures: {
...handlers,
...params.handlers,
// Core handlers
'initialize': server.initialize_handler,
'notifications/initialized': initialized_notification_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,
// Tool handlers
'tools/list': server.tools_list_handler,
'tools/call': server.tools_call_handler
}
})!

132
lib/mcp/handler_prompts.v Normal file
View File

@@ -0,0 +1,132 @@
module mcp
import time
import os
import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.mcp.logger
// 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 := jsonrpc.decode_request_generic[PromptGetParams](data)!
if !s.backend.prompt_exists(request.params.name)! {
return jsonrpc.new_error_response(request.id, prompt_not_found(request.params.name)).encode()
}
// Get the prompt by name
prompt := s.backend.prompt_get(request.params.name)!
// Validate required arguments
for arg in prompt.arguments {
if arg.required && (request.params.arguments[arg.name] == '') {
return jsonrpc.new_error_response(request.id, missing_required_argument(arg.name)).encode()
}
}
// 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.id, 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)}')
}

183
lib/mcp/handler_resources.v Normal file
View File

@@ -0,0 +1,183 @@
module mcp
import time
import os
import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.mcp.logger
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)}')
}

134
lib/mcp/handler_tools.v Normal file
View File

@@ -0,0 +1,134 @@
module mcp
import time
import os
import log
import x.json2
import json
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.mcp.logger
// Tool related structs
pub struct Tool {
pub:
name string
description string
input_schema ToolInputSchema @[json: 'inputSchema']
}
pub struct ToolInputSchema {
pub:
typ string @[json: 'type']
properties map[string]ToolProperty
required []string
}
pub struct ToolProperty {
pub:
typ string @[json: 'type']
items ToolItems
enum []string
}
pub struct ToolItems {
pub:
typ string @[json: 'type']
enum []string
}
pub struct ToolContent {
pub:
typ string @[json: 'type']
text string
}
// 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
// Create a success response with the result
response := jsonrpc.new_response_generic[ToolListResult](request.id, 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]string
}
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 := jsonrpc.decode_request_generic[ToolCallParams](data)!
if !s.backend.tool_exists(request.params.name)! {
return jsonrpc.new_error_response(request.id, tool_not_found(request.params.name)).encode()
}
// Get the tool by name
tool := s.backend.tool_get(request.params.name)!
// Validate arguments against the input schema
// TODO: Implement proper JSON Schema validation
for req in tool.input_schema.required {
if req !in request.params.arguments {
return jsonrpc.new_error_response(request.id, missing_required_argument(req)).encode()
}
}
// Call the tool with the provided arguments
result := s.backend.tool_call(request.params.name, request.params.arguments)!
// Create a success response with the result
response := jsonrpc.new_response_generic[ToolCallResult](request.id, 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)}')
}

View File

@@ -49,7 +49,7 @@ pub:
// ServerConfiguration represents the server configuration
pub struct ServerConfiguration {
pub:
protocol_version string = protocol_version @[json: 'protocolVersion']
protocol_version string = '2024-11-05' @[json: 'protocolVersion']
capabilities ServerCapabilities
server_info ServerInfo @[json: 'serverInfo']
}

35
lib/mcp/model_error.v Normal file
View File

@@ -0,0 +1,35 @@
module mcp
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'
}
}

View File

@@ -14,6 +14,7 @@ pub struct Server {
pub mut:
client_config ClientConfiguration
handler jsonrpc.Handler
backend Backend
}
// start starts the MCP server
@@ -41,7 +42,13 @@ pub fn (mut s Server) start() ! {
}
// Send the response
println(response)
flush_stdout()
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()
}

6
lib/mcp/tool_handler.v Normal file
View File

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

View File

@@ -0,0 +1,50 @@
module jsonrpc
// Notification represents a JSON-RPC 2.0 notification object.
// It contains all the required fields according to the JSON-RPC 2.0 specification.
// See: https://www.jsonrpc.org/specification#notification
pub struct Notification {
pub mut:
// The JSON-RPC protocol version, must be exactly "2.0"
jsonrpc string = "2.0" @[required]
// The name of the method to be invoked on the server
method string @[required]
}
// Notification represents a JSON-RPC 2.0 notification object.
// It contains all the required fields according to the JSON-RPC 2.0 specification.
// See: https://www.jsonrpc.org/specification#notification
pub struct NotificationGeneric[T] {
pub mut:
// The JSON-RPC protocol version, must be exactly "2.0"
jsonrpc string = "2.0" @[required]
// The name of the method to be invoked on the server
method string @[required]
params ?T
}
// new_notification creates a new JSON-RPC notification with the specified method and parameters.
// It automatically sets the JSON-RPC version to the current version.
//
// Parameters:
// - method: The name of the method to invoke on the server
// - params: The parameters to the method, encoded as a JSON string
//
// Returns:
// - A fully initialized Notification object
pub fn new_notification[T](method string, params T) NotificationGeneric[T] {
return NotificationGeneric[T]{
jsonrpc: jsonrpc.jsonrpc_version
method: method
params: params
}
}
pub fn new_blank_notification(method string) Notification {
return Notification{
jsonrpc: jsonrpc.jsonrpc_version
method: method
}
}