|
|
@@ -40,17 +40,26 @@ namespace Mcp.Tools {
|
|
|
public class Manager : GLib.Object {
|
|
|
private HashTable<string, Executor> executors;
|
|
|
private Variant? validation_schema;
|
|
|
+ private HashTable<string, Mcp.Tools.Types.ToolExecutionContext> active_executions;
|
|
|
+ private HashTable<string, Cancellable> 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<string, Executor> (str_hash, str_equal);
|
|
|
+ active_executions = new HashTable<string, Mcp.Tools.Types.ToolExecutionContext> (str_hash, str_equal);
|
|
|
+ progress_tokens = new HashTable<string, Cancellable> (str_hash, str_equal);
|
|
|
|
|
|
// Initialize basic validation schema
|
|
|
setup_validation_schema ();
|
|
|
@@ -64,6 +73,9 @@ namespace Mcp.Tools {
|
|
|
* @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));
|
|
|
}
|
|
|
@@ -75,6 +87,9 @@ namespace Mcp.Tools {
|
|
|
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);
|
|
|
|
|
|
@@ -141,49 +156,108 @@ namespace Mcp.Tools {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Handles the tools/list JSON-RPC method.
|
|
|
+ * Handles the tools/list JSON-RPC method with complete pagination support.
|
|
|
*
|
|
|
- * This method returns a list of all available tools with their definitions.
|
|
|
+ * 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
|
|
|
+ * @return The response result containing tools array and pagination info
|
|
|
* @throws Error If handling fails
|
|
|
*/
|
|
|
public async Variant handle_list (Variant @params) throws Error {
|
|
|
- var result = new Gee.ArrayList<Mcp.Tools.Types.ToolDefinition> ();
|
|
|
+ // 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 tool definitions from all executors
|
|
|
+ // Get all tool definitions from all executors
|
|
|
+ var all_tools = new Gee.ArrayList<Mcp.Tools.Types.ToolDefinition> ();
|
|
|
foreach (var name in executors.get_keys ()) {
|
|
|
var executor = executors.lookup (name);
|
|
|
|
|
|
try {
|
|
|
var definition = executor.get_definition ();
|
|
|
- result.add (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<Mcp.Tools.Types.ToolDefinition> ();
|
|
|
+ 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 result) {
|
|
|
+ foreach (var tool in page_tools) {
|
|
|
tools_builder.add_value (tool.to_variant ());
|
|
|
}
|
|
|
result_builder.add ("{sv}", "tools", tools_builder.end ());
|
|
|
|
|
|
- // Add pagination cursor if supported (for future implementation)
|
|
|
- // Only include nextCursor if it's not null
|
|
|
- if (@params.lookup_value ("cursor", null) != null) {
|
|
|
- string? next_cursor = null;
|
|
|
- // In a real implementation, this would calculate the actual next cursor
|
|
|
- // For now, we'll just not include it in the response
|
|
|
- if (next_cursor != null) {
|
|
|
- result_builder.add ("{sv}", "nextCursor", new Variant.string (next_cursor));
|
|
|
- }
|
|
|
+ // 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 ();
|
|
|
@@ -193,7 +267,8 @@ namespace Mcp.Tools {
|
|
|
* 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.
|
|
|
+ * 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
|
|
|
@@ -214,6 +289,15 @@ namespace Mcp.Tools {
|
|
|
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));
|
|
|
@@ -222,11 +306,45 @@ namespace Mcp.Tools {
|
|
|
// 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 {
|
|
|
- // Execute tool with timeout support
|
|
|
- var result = yield execute_with_timeout (executor, arguments);
|
|
|
+ // 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)
|
|
|
@@ -241,38 +359,168 @@ namespace Mcp.Tools {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Executes a tool with timeout support.
|
|
|
+ * Executes a tool with context and timeout support.
|
|
|
*
|
|
|
* @param executor The tool executor
|
|
|
- * @param arguments The tool arguments
|
|
|
+ * @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_timeout (Executor executor, Variant arguments) throws Error {
|
|
|
- // Default timeout of 30 seconds for tool execution
|
|
|
- const int TIMEOUT_SECONDS = 30;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- var source = new TimeoutSource (TIMEOUT_SECONDS * 1000);
|
|
|
+ // 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 {
|
|
|
- var result = yield executor.execute (arguments);
|
|
|
+ // 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");
|
|
|
+ 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.
|
|
|
*/
|
|
|
@@ -282,7 +530,7 @@ namespace Mcp.Tools {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Validates a tool's JSON schema.
|
|
|
+ * Validates a tool's JSON schema with comprehensive checks.
|
|
|
*
|
|
|
* @param schema The schema to validate
|
|
|
* @throws Error If the schema is invalid
|
|
|
@@ -296,41 +544,803 @@ namespace Mcp.Tools {
|
|
|
throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool schema must have a 'type' property");
|
|
|
}
|
|
|
|
|
|
- if (schema.lookup_value ("type", VariantType.STRING).get_string () != "object") {
|
|
|
+ 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 arguments against a JSON schema.
|
|
|
+ * 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 {
|
|
|
- // Basic validation - in a full implementation, this would use
|
|
|
- // proper schema validation capabilities
|
|
|
-
|
|
|
if (!schema.is_of_type (VariantType.VARDICT)) {
|
|
|
return; // Invalid schema format, skip validation
|
|
|
}
|
|
|
|
|
|
- if (schema.lookup_value ("required", null) == null) {
|
|
|
- return; // No required properties to check
|
|
|
+ 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);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- var required = schema.lookup_value ("required", new VariantType ("as"));
|
|
|
- if (!required.is_of_type (new VariantType ("as"))) {
|
|
|
- return; // Invalid required format, skip validation
|
|
|
+ // 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<string> ();
|
|
|
+
|
|
|
+ // 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);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- 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) {
|
|
|
+ // 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 (
|
|
|
- "Missing required property: %s".printf (required_prop)
|
|
|
+ "Unexpected property: %s".printf (prop_name)
|
|
|
);
|
|
|
}
|
|
|
}
|