/* * Copyright (C) 2025 Mcp-Vala Project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Author: Mcp-Vala Project */ /** * Tool manager for MCP protocol. * * This class manages tool executors and handles tool-related * JSON-RPC method calls. It provides a centralized way to register, * discover, and execute tools while handling validation and error cases. * * The manager supports both synchronous and asynchronous tool execution, * argument validation against JSON schemas, and proper error handling. */ namespace Mcp.Tools { /** * Tool manager implementation. * * This class implements the IToolManager interface and provides * complete tool lifecycle management including registration, discovery, * execution, and validation. */ public class Manager : GLib.Object { private HashTable executors; private Variant? validation_schema; private HashTable active_executions; private HashTable progress_tokens; /** * Signal emitted when the tool list changes. */ public signal void list_changed (); /** * Signal emitted when tool execution progress is updated. */ public signal void progress_updated (string progress_token, double progress, string? message = null); /** * Creates a new Manager. */ public Manager () { executors = new HashTable (str_hash, str_equal); active_executions = new HashTable (str_hash, str_equal); progress_tokens = new HashTable (str_hash, str_equal); // Initialize basic validation schema setup_validation_schema (); } /** * Registers a tool executor. * * @param name The tool name * @param executor The executor implementation * @throws Error If registration fails (e.g., duplicate name) */ public void register_executor (string name, Executor executor) throws Error { // Validate tool name according to MCP specification validate_tool_name (name); if (executors.contains (name)) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool already registered: %s".printf (name)); } // Validate executor try { var definition = executor.get_definition (); if (definition.name != name) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool definition name mismatch: %s != %s".printf (definition.name, name)); } // Validate tool definition name as well validate_tool_name (definition.name); // Validate input schema validate_tool_schema (definition.input_schema); executors.insert (name, executor); list_changed (); } catch (Error e) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Failed to register tool %s: %s".printf (name, e.message)); } } /** * Unregisters a tool executor. * * @param name The tool name to unregister * @return True if the tool was found and removed */ public bool unregister_executor (string name) { bool removed = executors.remove (name); if (removed) { list_changed (); } return removed; } /** * Gets a registered tool executor. * * @param name The tool name * @return The executor or null if not found */ public Executor? get_executor (string name) { return executors.lookup (name); } /** * Lists all registered tool names. * * @return Array of tool names */ public string[] list_tools () { var names = new string[executors.size ()]; int i = 0; foreach (var name in executors.get_keys ()) { names[i++] = name; } return names; } /** * Validates tool arguments against the tool's schema. * * @param tool_name The name of the tool * @param arguments The arguments to validate * @throws Error If validation fails */ public void validate_arguments (string tool_name, Variant arguments) throws Error { var executor = executors.lookup (tool_name); if (executor == null) { throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool not found: %s".printf (tool_name)); } var definition = executor.get_definition (); validate_against_schema (arguments, definition.input_schema); } /** * Handles the tools/list JSON-RPC method with complete pagination support. * * This method returns a paginated list of all available tools with their definitions. * Each tool includes its name, description, and input schema. * * @param params The method parameters (may include cursor for pagination) * @return The response result containing tools array and pagination info * @throws Error If handling fails */ public async Variant handle_list (Variant @params) throws Error { // Extract cursor parameter if present string? cursor = null; if (@params.lookup_value ("cursor", null) != null) { cursor = @params.lookup_value ("cursor", VariantType.STRING).get_string (); } // Get all tool definitions from all executors var all_tools = new Gee.ArrayList (); foreach (var name in executors.get_keys ()) { var executor = executors.lookup (name); try { var definition = executor.get_definition (); all_tools.add (definition); } catch (Error e) { // Continue with other executors } } // Sort tools by name for consistent pagination all_tools.sort ((a, b) => { return strcmp (a.name, b.name); }); // Pagination settings const int PAGE_SIZE = 20; // Tools per page int start_index = 0; // Parse cursor to determine starting position if (cursor != null) { // Validate cursor format - should be a base64-encoded position try { // Decode base64 cursor to get position uint8[] cursor_bytes = Base64.decode (cursor); if (cursor_bytes.length == 4) { // Convert 4 bytes to int (little-endian) start_index = cursor_bytes[0] | (cursor_bytes[1] << 8) | (cursor_bytes[2] << 16) | (cursor_bytes[3] << 24); } else { // Fallback to simple integer parsing for backward compatibility start_index = int.parse (cursor); } } catch (Error e) { // Invalid cursor, start from beginning start_index = 0; } // Validate start_index bounds if (start_index < 0) { start_index = 0; } else if (start_index >= all_tools.size) { // Cursor beyond available tools, return empty result var result_builder = Mcp.Types.VariantUtils.new_dict_builder (); var tools_builder = Mcp.Types.VariantUtils.new_dict_array_builder (); result_builder.add ("{sv}", "tools", tools_builder.end ()); return result_builder.end (); } } // Calculate the end index for this page int end_index = int.min (start_index + PAGE_SIZE, all_tools.size); // Get the tools for this page var page_tools = new Gee.ArrayList (); for (int i = start_index; i < end_index; i++) { page_tools.add (all_tools[i]); } // Build response as Variant using utility functions var result_builder = Mcp.Types.VariantUtils.new_dict_builder (); // Serialize tools array var tools_builder = Mcp.Types.VariantUtils.new_dict_array_builder (); foreach (var tool in page_tools) { tools_builder.add_value (tool.to_variant ()); } result_builder.add ("{sv}", "tools", tools_builder.end ()); // Add nextCursor if there are more tools if (end_index < all_tools.size) { // Encode next position as base64 for proper cursor implementation uint8[] cursor_bytes = new uint8[4]; int next_position = end_index; cursor_bytes[0] = (uint8) (next_position & 0xFF); cursor_bytes[1] = (uint8) ((next_position >> 8) & 0xFF); cursor_bytes[2] = (uint8) ((next_position >> 16) & 0xFF); cursor_bytes[3] = (uint8) ((next_position >> 24) & 0xFF); string next_cursor = Base64.encode (cursor_bytes); result_builder.add ("{sv}", "nextCursor", new Variant.string (next_cursor)); } return result_builder.end (); } /** * Handles the tools/call JSON-RPC method. * * This method executes a tool with the provided arguments after validation. * It supports both synchronous and asynchronous tool execution with progress * reporting and cancellation support. * * @param params The method parameters containing tool name and arguments * @return The response result containing tool execution output * @throws Error If handling fails */ public async Variant handle_call (Variant @params) throws Error { if (@params.lookup_value ("name", null) == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Missing name parameter"); } string name = @params.lookup_value ("name", VariantType.STRING).get_string (); Variant? arguments = null; if (@params.lookup_value ("arguments", null) != null) { arguments = @params.lookup_value ("arguments", VariantType.VARDICT); } else { // Create empty arguments object if not provided using utility function arguments = Mcp.Types.VariantUtils.new_empty_dict (); } // Check for progress token string? progress_token = null; if (@params.lookup_value ("_meta", null) != null) { var meta = @params.lookup_value ("_meta", VariantType.VARDICT); if (meta != null && meta.lookup_value ("progressToken", null) != null) { progress_token = meta.lookup_value ("progressToken", VariantType.STRING).get_string (); } } Executor? executor = executors.lookup (name); if (executor == null) { throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool not found: %s".printf (name)); } // Validate arguments against tool schema validate_arguments (name, arguments); // Create execution context string execution_id = generate_execution_id (); var context = new Mcp.Tools.Types.ToolExecutionContext ( execution_id, name, arguments, progress_token ); active_executions.insert (execution_id, context); // Register progress token if provided if (progress_token != null) { progress_tokens.insert (progress_token, context.get_cancellable ()); } try { // Mark execution as started context.mark_started (); // Execute tool with context support var result = yield execute_with_context (executor, context); // Mark execution as completed context.mark_completed (); // Clean up active_executions.remove (execution_id); if (progress_token != null) { progress_tokens.remove (progress_token); } return result.to_variant (); } catch (Error e) { // Mark execution as failed context.mark_failed (); // Clean up active_executions.remove (execution_id); if (progress_token != null) { progress_tokens.remove (progress_token); } // Create error result var error_content = new Mcp.Types.Common.TextContent ( "Tool execution failed: %s".printf (e.message) ); var error_result = new Mcp.Tools.Types.CallToolResult (); error_result.content.add (error_content); error_result.is_error = true; return error_result.to_variant (); } } /** * Executes a tool with context and timeout support. * * @param executor The tool executor * @param context The execution context * @return The execution result * @throws Error If execution fails or times out */ private async Mcp.Tools.Types.CallToolResult execute_with_context (Executor executor, Mcp.Tools.Types.ToolExecutionContext context) throws Error { // Get timeout from tool definition or use default int timeout_seconds = 30; var definition = executor.get_definition (); if (definition.execution != null && definition.execution.timeout_seconds > 0) { timeout_seconds = definition.execution.timeout_seconds; } // Create timeout source var source = new TimeoutSource (timeout_seconds * 1000); bool timed_out = false; source.set_callback (() => { timed_out = true; context.cancel (); return false; // Remove timeout source }); source.attach (null); try { // Execute tool with cancellable support var result = yield executor.execute_with_context (context.arguments, context.get_cancellable ()); source.destroy (); if (timed_out) { throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool execution timed out after %d seconds".printf (timeout_seconds)); } if (context.is_cancelled) { throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool execution was cancelled"); } return result; } catch (Error e) { source.destroy (); // Check if error is due to cancellation if (e is IOError.CANCELLED || context.is_cancelled) { throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool execution was cancelled"); } throw e; } } /** * Generates a unique execution ID. * * @return A unique execution ID */ private string generate_execution_id () { var timestamp = new DateTime.now_utc ().to_unix (); var random = GLib.Random.next_int (); return "exec_%lld_%u".printf (timestamp, random); } /** * Reports progress for a tool execution. * * @param progress_token The progress token * @param progress Current progress value (0-100) * @param message Optional progress message * @throws Error If the progress token is not found */ public void report_progress (string progress_token, double progress, string? message = null) throws Error { if (progress_tokens.contains (progress_token)) { progress_updated (progress_token, progress, message); } else { throw new Mcp.Core.McpError.INVALID_PARAMS ("Invalid progress token: %s".printf (progress_token)); } } /** * Cancels a tool execution using its progress token. * * @param progress_token The progress token of the execution to cancel * @throws Error If the progress token is not found */ public void cancel_execution (string progress_token) throws Error { if (progress_tokens.contains (progress_token)) { var cancellable = progress_tokens.lookup (progress_token); cancellable.cancel (); } else { throw new Mcp.Core.McpError.INVALID_PARAMS ("Invalid progress token: %s".printf (progress_token)); } } /** * Gets the execution context for a given execution ID. * * @param execution_id The execution ID * @return The execution context or null if not found */ public Mcp.Tools.Types.ToolExecutionContext? get_execution_context (string execution_id) { return active_executions.lookup (execution_id); } /** * Gets all active execution contexts. * * @return Array of active execution contexts */ public Mcp.Tools.Types.ToolExecutionContext[] get_active_executions () { var contexts = new Mcp.Tools.Types.ToolExecutionContext[active_executions.size ()]; int i = 0; foreach (var context in active_executions.get_values ()) { contexts[i++] = context; } return contexts; } /** * Validates a tool name according to MCP specification. * * @param name The tool name to validate * @throws Error If name is invalid */ private void validate_tool_name (string name) throws Error { // Check if name is null or empty if (name == null || name.strip () == "") { throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool name cannot be null or empty"); } // Check length (1-128 characters) if (name.length < 1 || name.length > 128) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Tool name must be between 1 and 128 characters long, got %d characters".printf (name.length) ); } // Check for allowed characters: ASCII letters, digits, underscore, hyphen, and dot // Regex pattern: ^[a-zA-Z0-9_.-]+$ try { Regex name_regex = new Regex ("^[a-zA-Z0-9_.-]+$"); if (!name_regex.match (name)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Tool name contains invalid characters. Only ASCII letters, digits, underscore, hyphen, and dot are allowed" ); } } catch (Error e) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Failed to validate tool name: %s".printf (e.message) ); } // Check for spaces, commas, or other special characters (additional safety check) foreach (char c in name.to_utf8 ()) { if (c == ' ' || c == ',' || c == ';' || c == ':' || c == '/' || c == '\\') { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Tool name contains invalid character '%c'. Spaces, commas, and other special characters are not allowed".printf (c) ); } } } /** * Sets up the basic JSON schema validation. */ private void setup_validation_schema () { // This would set up a basic schema for validation // For now, we'll use simple validation methods } /** * Validates a tool's JSON schema with comprehensive checks. * * @param schema The schema to validate * @throws Error If the schema is invalid */ private void validate_tool_schema (Variant schema) throws Error { if (!schema.is_of_type (VariantType.VARDICT)) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool schema must be a dictionary"); } if (schema.lookup_value ("type", null) == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool schema must have a 'type' property"); } string schema_type = schema.lookup_value ("type", VariantType.STRING).get_string (); if (schema_type != "object") { throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool schema type must be 'object'"); } // Validate properties if present if (schema.lookup_value ("properties", null) != null) { var properties = schema.lookup_value ("properties", VariantType.VARDICT); if (!properties.is_of_type (VariantType.VARDICT)) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Schema properties must be a dictionary"); } // Validate each property schema validate_property_schemas (properties); } // Validate required array if present if (schema.lookup_value ("required", null) != null) { var required = schema.lookup_value ("required", new VariantType ("as")); if (!required.is_of_type (new VariantType ("as"))) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Schema required must be an array of strings"); } } } /** * Validates property schemas within a schema. * * @param properties The properties dictionary to validate * @throws Error If any property schema is invalid */ private void validate_property_schemas (Variant properties) throws Error { var iter = properties.iterator (); string? prop_name; Variant? prop_schema; while (iter.next ("{sv}", out prop_name, out prop_schema)) { if (prop_name == null || prop_schema == null) { continue; } if (!prop_schema.is_of_type (VariantType.VARDICT)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property schema for '%s' must be a dictionary".printf (prop_name) ); } // Check for type property if (prop_schema.lookup_value ("type", null) == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property schema for '%s' must have a 'type' property".printf (prop_name) ); } // Validate type-specific constraints validate_property_constraints (prop_name, prop_schema); } } /** * Validates type-specific constraints for a property. * * @param prop_name The property name * @param prop_schema The property schema * @throws Error If constraints are invalid */ private void validate_property_constraints (string prop_name, Variant prop_schema) throws Error { string prop_type = prop_schema.lookup_value ("type", VariantType.STRING).get_string (); switch (prop_type) { case "string": validate_string_constraints (prop_name, prop_schema); break; case "number": case "integer": validate_numeric_constraints (prop_name, prop_schema); break; case "array": validate_array_constraints (prop_name, prop_schema); break; case "object": validate_object_constraints (prop_name, prop_schema); break; case "boolean": // Boolean type has no additional constraints break; default: throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid property type '%s' for property '%s'".printf (prop_type, prop_name) ); } } /** * Validates string-specific constraints. * * @param prop_name The property name * @param prop_schema The property schema * @throws Error If constraints are invalid */ private void validate_string_constraints (string prop_name, Variant prop_schema) throws Error { // Check minLength if present if (prop_schema.lookup_value ("minLength", null) != null) { var min_length = prop_schema.lookup_value ("minLength", VariantType.INT32); if (!min_length.is_of_type (VariantType.INT32) || min_length.get_int32 () < 0) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid minLength for property '%s': must be a non-negative integer".printf (prop_name) ); } } // Check maxLength if present if (prop_schema.lookup_value ("maxLength", null) != null) { var max_length = prop_schema.lookup_value ("maxLength", VariantType.INT32); if (!max_length.is_of_type (VariantType.INT32) || max_length.get_int32 () < 0) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid maxLength for property '%s': must be a non-negative integer".printf (prop_name) ); } } // Check pattern if present if (prop_schema.lookup_value ("pattern", null) != null) { var pattern = prop_schema.lookup_value ("pattern", VariantType.STRING); if (!pattern.is_of_type (VariantType.STRING)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid pattern for property '%s': must be a string".printf (prop_name) ); } // Validate regex pattern (basic check) try { // Simple validation - try to compile the regex Regex regex = new Regex (pattern.get_string ()); } catch (Error e) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid pattern for property '%s': %s".printf (prop_name, e.message) ); } } // Check format if present if (prop_schema.lookup_value ("format", null) != null) { var format = prop_schema.lookup_value ("format", VariantType.STRING); if (!format.is_of_type (VariantType.STRING)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid format for property '%s': must be a string".printf (prop_name) ); } // Validate supported formats string format_str = format.get_string (); if (!is_supported_format (format_str)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Unsupported format '%s' for property '%s'".printf (format_str, prop_name) ); } } } /** * Validates numeric-specific constraints. * * @param prop_name The property name * @param prop_schema The property schema * @throws Error If constraints are invalid */ private void validate_numeric_constraints (string prop_name, Variant prop_schema) throws Error { // Check minimum if present if (prop_schema.lookup_value ("minimum", null) != null) { var minimum = prop_schema.lookup_value ("minimum", VariantType.DOUBLE); if (!minimum.is_of_type (VariantType.DOUBLE)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid minimum for property '%s': must be a number".printf (prop_name) ); } } // Check maximum if present if (prop_schema.lookup_value ("maximum", null) != null) { var maximum = prop_schema.lookup_value ("maximum", VariantType.DOUBLE); if (!maximum.is_of_type (VariantType.DOUBLE)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid maximum for property '%s': must be a number".printf (prop_name) ); } } // Check exclusiveMinimum if present if (prop_schema.lookup_value ("exclusiveMinimum", null) != null) { var excl_min = prop_schema.lookup_value ("exclusiveMinimum", VariantType.DOUBLE); if (!excl_min.is_of_type (VariantType.DOUBLE)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid exclusiveMinimum for property '%s': must be a number".printf (prop_name) ); } } // Check exclusiveMaximum if present if (prop_schema.lookup_value ("exclusiveMaximum", null) != null) { var excl_max = prop_schema.lookup_value ("exclusiveMaximum", VariantType.DOUBLE); if (!excl_max.is_of_type (VariantType.DOUBLE)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid exclusiveMaximum for property '%s': must be a number".printf (prop_name) ); } } // Check multipleOf if present if (prop_schema.lookup_value ("multipleOf", null) != null) { var multiple_of = prop_schema.lookup_value ("multipleOf", VariantType.DOUBLE); if (!multiple_of.is_of_type (VariantType.DOUBLE) || multiple_of.get_double () <= 0) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid multipleOf for property '%s': must be a positive number".printf (prop_name) ); } } } /** * Validates array-specific constraints. * * @param prop_name The property name * @param prop_schema The property schema * @throws Error If constraints are invalid */ private void validate_array_constraints (string prop_name, Variant prop_schema) throws Error { // Check minItems if present if (prop_schema.lookup_value ("minItems", null) != null) { var min_items = prop_schema.lookup_value ("minItems", VariantType.INT32); if (!min_items.is_of_type (VariantType.INT32) || min_items.get_int32 () < 0) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid minItems for property '%s': must be a non-negative integer".printf (prop_name) ); } } // Check maxItems if present if (prop_schema.lookup_value ("maxItems", null) != null) { var max_items = prop_schema.lookup_value ("maxItems", VariantType.INT32); if (!max_items.is_of_type (VariantType.INT32) || max_items.get_int32 () < 0) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid maxItems for property '%s': must be a non-negative integer".printf (prop_name) ); } } // Check items schema if present if (prop_schema.lookup_value ("items", null) != null) { var items = prop_schema.lookup_value ("items", VariantType.VARDICT); if (!items.is_of_type (VariantType.VARDICT)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid items for property '%s': must be a schema object".printf (prop_name) ); } // Recursively validate items schema validate_property_constraints (prop_name + "[]", items); } } /** * Validates object-specific constraints. * * @param prop_name The property name * @param prop_schema The property schema * @throws Error If constraints are invalid */ private void validate_object_constraints (string prop_name, Variant prop_schema) throws Error { // Check properties if present if (prop_schema.lookup_value ("properties", null) != null) { var properties = prop_schema.lookup_value ("properties", VariantType.VARDICT); if (!properties.is_of_type (VariantType.VARDICT)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid properties for property '%s': must be a dictionary".printf (prop_name) ); } // Recursively validate property schemas validate_property_schemas (properties); } // Check required if present if (prop_schema.lookup_value ("required", null) != null) { var required = prop_schema.lookup_value ("required", new VariantType ("as")); if (!required.is_of_type (new VariantType ("as"))) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid required for property '%s': must be an array of strings".printf (prop_name) ); } } } /** * Checks if a format string is supported. * * @param format The format string to check * @return True if the format is supported */ private bool is_supported_format (string format) { // List of supported JSON Schema formats string[] supported_formats = { "date-time", "date", "time", "email", "hostname", "ipv4", "ipv6", "uri", "uuid" }; foreach (var supported in supported_formats) { if (format == supported) { return true; } } return false; } /** * Validates arguments against a JSON schema with comprehensive checks. * * @param arguments The arguments to validate * @param schema The schema to validate against * @throws Error If validation fails */ private void validate_against_schema (Variant arguments, Variant schema) throws Error { if (!schema.is_of_type (VariantType.VARDICT)) { return; // Invalid schema format, skip validation } if (!arguments.is_of_type (VariantType.VARDICT)) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Arguments must be a dictionary"); } // Check required properties if (schema.lookup_value ("required", null) != null) { var required = schema.lookup_value ("required", new VariantType ("as")); if (required.is_of_type (new VariantType ("as"))) { var required_array = required.get_strv (); for (int i = 0; i < required_array.length; i++) { var required_prop = required_array[i]; if (arguments.lookup_value (required_prop, null) == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Missing required property: %s".printf (required_prop) ); } } } } // Validate each argument against its schema if (schema.lookup_value ("properties", null) != null) { var properties = schema.lookup_value ("properties", VariantType.VARDICT); if (properties.is_of_type (VariantType.VARDICT)) { validate_arguments_against_properties (arguments, properties); } } // Check for additional properties if allowed if (schema.lookup_value ("additionalProperties", null) != null) { var additional_props = schema.lookup_value ("additionalProperties", null); if (additional_props.is_of_type (VariantType.BOOLEAN) && !additional_props.get_boolean ()) { // No additional properties allowed, check for unexpected properties check_unexpected_properties (arguments, schema); } } } /** * Validates arguments against property schemas. * * @param arguments The arguments to validate * @param properties The property schemas * @throws Error If validation fails */ private void validate_arguments_against_properties (Variant arguments, Variant properties) throws Error { var iter = properties.iterator (); string? prop_name; Variant? prop_schema; while (iter.next ("{sv}", out prop_name, out prop_schema)) { if (prop_name == null || prop_schema == null) { continue; } // Check if property is present in arguments var arg_value = arguments.lookup_value (prop_name, null); if (arg_value != null) { // Validate the argument value against the property schema validate_value_against_schema (arg_value, prop_schema, prop_name); } } } /** * Validates a value against a property schema. * * @param value The value to validate * @param schema The schema to validate against * @param prop_name The property name for error messages * @throws Error If validation fails */ private void validate_value_against_schema (Variant value, Variant schema, string prop_name) throws Error { if (!schema.is_of_type (VariantType.VARDICT)) { return; // Invalid schema, skip validation } if (schema.lookup_value ("type", null) == null) { return; // No type specified, skip validation } string expected_type = schema.lookup_value ("type", VariantType.STRING).get_string (); // Check type compatibility if (!is_type_compatible (value, expected_type)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' has invalid type: expected %s".printf (prop_name, expected_type) ); } // Perform type-specific validation switch (expected_type) { case "string": validate_string_value (value, schema, prop_name); break; case "number": case "integer": validate_numeric_value (value, schema, prop_name); break; case "array": validate_array_value (value, schema, prop_name); break; case "object": validate_object_value (value, schema, prop_name); break; case "boolean": // Boolean values don't need additional validation break; } } /** * Checks if a Variant value is compatible with an expected JSON Schema type. * * @param value The value to check * @param expected_type The expected type * @return True if compatible */ private bool is_type_compatible (Variant value, string expected_type) { switch (expected_type) { case "string": return value.is_of_type (VariantType.STRING); case "number": return value.is_of_type (VariantType.DOUBLE) || value.is_of_type (VariantType.INT32) || value.is_of_type (VariantType.INT64) || value.is_of_type (VariantType.UINT32) || value.is_of_type (VariantType.UINT64); case "integer": return value.is_of_type (VariantType.INT32) || value.is_of_type (VariantType.INT64) || value.is_of_type (VariantType.UINT32) || value.is_of_type (VariantType.UINT64); case "boolean": return value.is_of_type (VariantType.BOOLEAN); case "array": return value.is_of_type (new VariantType ("av")) || value.is_of_type (new VariantType ("aa{sv}")); case "object": return value.is_of_type (VariantType.VARDICT); default: return false; } } /** * Validates a string value against string constraints. * * @param value The value to validate * @param schema The schema to validate against * @param prop_name The property name * @throws Error If validation fails */ private void validate_string_value (Variant value, Variant schema, string prop_name) throws Error { string str_value = value.get_string (); // Check minLength if (schema.lookup_value ("minLength", null) != null) { int min_length = schema.lookup_value ("minLength", VariantType.INT32).get_int32 (); if (str_value.length < min_length) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' is too short: minimum length is %d".printf (prop_name, min_length) ); } } // Check maxLength if (schema.lookup_value ("maxLength", null) != null) { int max_length = schema.lookup_value ("maxLength", VariantType.INT32).get_int32 (); if (str_value.length > max_length) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' is too long: maximum length is %d".printf (prop_name, max_length) ); } } // Check pattern if (schema.lookup_value ("pattern", null) != null) { string pattern = schema.lookup_value ("pattern", VariantType.STRING).get_string (); try { Regex regex = new Regex (pattern); if (!regex.match (str_value)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' does not match required pattern".printf (prop_name) ); } } catch (Error e) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Invalid pattern for property '%s': %s".printf (prop_name, e.message) ); } } // Check format if (schema.lookup_value ("format", null) != null) { string format = schema.lookup_value ("format", VariantType.STRING).get_string (); if (!validate_format (str_value, format)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' does not match required format: %s".printf (prop_name, format) ); } } } /** * Validates a numeric value against numeric constraints. * * @param value The value to validate * @param schema The schema to validate against * @param prop_name The property name * @throws Error If validation fails */ private void validate_numeric_value (Variant value, Variant schema, string prop_name) throws Error { double num_value; if (value.is_of_type (VariantType.DOUBLE)) { num_value = value.get_double (); } else if (value.is_of_type (VariantType.INT32)) { num_value = (double) value.get_int32 (); } else if (value.is_of_type (VariantType.INT64)) { num_value = (double) value.get_int64 (); } else if (value.is_of_type (VariantType.UINT32)) { num_value = (double) value.get_uint32 (); } else if (value.is_of_type (VariantType.UINT64)) { num_value = (double) value.get_uint64 (); } else { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' has invalid numeric type".printf (prop_name) ); } // Check minimum if (schema.lookup_value ("minimum", null) != null) { double minimum = schema.lookup_value ("minimum", VariantType.DOUBLE).get_double (); if (num_value < minimum) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' is too small: minimum is %g".printf (prop_name, minimum) ); } } // Check maximum if (schema.lookup_value ("maximum", null) != null) { double maximum = schema.lookup_value ("maximum", VariantType.DOUBLE).get_double (); if (num_value > maximum) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' is too large: maximum is %g".printf (prop_name, maximum) ); } } // Check exclusiveMinimum if (schema.lookup_value ("exclusiveMinimum", null) != null) { double excl_min = schema.lookup_value ("exclusiveMinimum", VariantType.DOUBLE).get_double (); if (num_value <= excl_min) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' must be greater than %g".printf (prop_name, excl_min) ); } } // Check exclusiveMaximum if (schema.lookup_value ("exclusiveMaximum", null) != null) { double excl_max = schema.lookup_value ("exclusiveMaximum", VariantType.DOUBLE).get_double (); if (num_value >= excl_max) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' must be less than %g".printf (prop_name, excl_max) ); } } // Check multipleOf if (schema.lookup_value ("multipleOf", null) != null) { double multiple_of = schema.lookup_value ("multipleOf", VariantType.DOUBLE).get_double (); if (multiple_of > 0) { double remainder = num_value % multiple_of; if (remainder > 1e-10 && remainder < (multiple_of - 1e-10)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' must be a multiple of %g".printf (prop_name, multiple_of) ); } } } // Check integer constraint if (schema.lookup_value ("type", VariantType.STRING).get_string () == "integer") { if (num_value != Math.floor (num_value)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' must be an integer".printf (prop_name) ); } } } /** * Validates an array value against array constraints. * * @param value The value to validate * @param schema The schema to validate against * @param prop_name The property name * @throws Error If validation fails */ private void validate_array_value (Variant value, Variant schema, string prop_name) throws Error { Variant array_value; if (value.is_of_type (new VariantType ("av"))) { array_value = value; } else if (value.is_of_type (new VariantType ("aa{sv}"))) { array_value = value; } else { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' has invalid array type".printf (prop_name) ); } size_t array_length = array_value.n_children (); // Check minItems if (schema.lookup_value ("minItems", null) != null) { int min_items = schema.lookup_value ("minItems", VariantType.INT32).get_int32 (); if (array_length < min_items) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' has too few items: minimum is %d".printf (prop_name, min_items) ); } } // Check maxItems if (schema.lookup_value ("maxItems", null) != null) { int max_items = schema.lookup_value ("maxItems", VariantType.INT32).get_int32 (); if (array_length > max_items) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' has too many items: maximum is %d".printf (prop_name, max_items) ); } } // Validate items if schema is provided if (schema.lookup_value ("items", null) != null) { var items_schema = schema.lookup_value ("items", VariantType.VARDICT); if (items_schema.is_of_type (VariantType.VARDICT)) { for (size_t i = 0; i < array_length; i++) { var item_value = array_value.get_child_value (i); validate_value_against_schema (item_value, items_schema, "%s[%zu]".printf (prop_name, i)); } } } } /** * Validates an object value against object constraints. * * @param value The value to validate * @param schema The schema to validate against * @param prop_name The property name * @throws Error If validation fails */ private void validate_object_value (Variant value, Variant schema, string prop_name) throws Error { if (!value.is_of_type (VariantType.VARDICT)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' has invalid object type".printf (prop_name) ); } // Validate properties if schema is provided if (schema.lookup_value ("properties", null) != null) { var properties_schema = schema.lookup_value ("properties", VariantType.VARDICT); if (properties_schema.is_of_type (VariantType.VARDICT)) { validate_arguments_against_properties (value, properties_schema); } } // Check required properties if (schema.lookup_value ("required", null) != null) { var required = schema.lookup_value ("required", new VariantType ("as")); if (required.is_of_type (new VariantType ("as"))) { var required_array = required.get_strv (); for (int i = 0; i < required_array.length; i++) { var required_prop = required_array[i]; if (value.lookup_value (required_prop, null) == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Property '%s' is missing required property: %s".printf (prop_name, required_prop) ); } } } } } /** * Validates a string value against a format. * * @param value The value to validate * @param format The format to validate against * @return True if valid */ private bool validate_format (string value, string format) { switch (format) { case "email": // Basic email validation return Regex.match_simple ("^[^@]+@[^@]+\\.[^@]+$", value); case "uri": // Basic URI validation return Regex.match_simple ("^[a-zA-Z][a-zA-Z0-9+.-]*://", value); case "uuid": // UUID validation return Regex.match_simple ("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", value); case "ipv4": // IPv4 validation return Regex.match_simple ("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value); case "ipv6": // IPv6 validation (basic) return Regex.match_simple ("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$", value); case "date-time": // ISO 8601 date-time validation (basic) return Regex.match_simple ("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", value); case "date": // ISO 8601 date validation (basic) return Regex.match_simple ("^\\d{4}-\\d{2}-\\d{2}$", value); case "time": // ISO 8601 time validation (basic) return Regex.match_simple ("^\\d{2}:\\d{2}:\\d{2}$", value); case "hostname": // Hostname validation (basic) return Regex.match_simple ("^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?$", value); default: // Unknown format, assume valid return true; } } /** * Checks for unexpected properties in arguments. * * @param arguments The arguments to check * @param schema The schema defining allowed properties * @throws Error If unexpected properties are found */ private void check_unexpected_properties (Variant arguments, Variant schema) throws Error { if (schema.lookup_value ("properties", null) == null) { // No properties defined, any property is unexpected var iter = arguments.iterator (); string? prop_name; Variant? prop_value; while (iter.next ("{sv}", out prop_name, out prop_value)) { if (prop_name != null) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Unexpected property: %s".printf (prop_name) ); } } return; } var properties = schema.lookup_value ("properties", VariantType.VARDICT); var allowed_props = new Gee.HashSet (); // Collect all allowed property names var iter = properties.iterator (); string? prop_name; Variant? prop_schema; while (iter.next ("{sv}", out prop_name, out prop_schema)) { if (prop_name != null) { allowed_props.add (prop_name); } } // Check for unexpected properties iter = arguments.iterator (); Variant? prop_value; while (iter.next ("{sv}", out prop_name, out prop_value)) { if (prop_name != null && !allowed_props.contains (prop_name)) { throw new Mcp.Core.McpError.INVALID_PARAMS ( "Unexpected property: %s".printf (prop_name) ); } } } } }