| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349 |
- /*
- * 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<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 ();
- }
-
- /**
- * 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<Mcp.Tools.Types.ToolDefinition> ();
- 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<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 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<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);
- }
- }
-
- // 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)
- );
- }
- }
- }
- }
- }
|