manager.vala 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349
  1. /*
  2. * Copyright (C) 2025 Mcp-Vala Project
  3. *
  4. * This library is free software; you can redistribute it and/or
  5. * modify it under the terms of the GNU Lesser General Public
  6. * License as published by the Free Software Foundation; either
  7. * version 2.1 of the License, or (at your option) any later version.
  8. *
  9. * This library is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. * Lesser General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Lesser General Public
  15. * License along with this library; if not, write to the Free Software
  16. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  17. *
  18. * Author: Mcp-Vala Project
  19. */
  20. /**
  21. * Tool manager for MCP protocol.
  22. *
  23. * This class manages tool executors and handles tool-related
  24. * JSON-RPC method calls. It provides a centralized way to register,
  25. * discover, and execute tools while handling validation and error cases.
  26. *
  27. * The manager supports both synchronous and asynchronous tool execution,
  28. * argument validation against JSON schemas, and proper error handling.
  29. */
  30. namespace Mcp.Tools {
  31. /**
  32. * Tool manager implementation.
  33. *
  34. * This class implements the IToolManager interface and provides
  35. * complete tool lifecycle management including registration, discovery,
  36. * execution, and validation.
  37. */
  38. public class Manager : GLib.Object {
  39. private HashTable<string, Executor> executors;
  40. private Variant? validation_schema;
  41. private HashTable<string, Mcp.Tools.Types.ToolExecutionContext> active_executions;
  42. private HashTable<string, Cancellable> progress_tokens;
  43. /**
  44. * Signal emitted when the tool list changes.
  45. */
  46. public signal void list_changed ();
  47. /**
  48. * Signal emitted when tool execution progress is updated.
  49. */
  50. public signal void progress_updated (string progress_token, double progress, string? message = null);
  51. /**
  52. * Creates a new Manager.
  53. */
  54. public Manager () {
  55. executors = new HashTable<string, Executor> (str_hash, str_equal);
  56. active_executions = new HashTable<string, Mcp.Tools.Types.ToolExecutionContext> (str_hash, str_equal);
  57. progress_tokens = new HashTable<string, Cancellable> (str_hash, str_equal);
  58. // Initialize basic validation schema
  59. setup_validation_schema ();
  60. }
  61. /**
  62. * Registers a tool executor.
  63. *
  64. * @param name The tool name
  65. * @param executor The executor implementation
  66. * @throws Error If registration fails (e.g., duplicate name)
  67. */
  68. public void register_executor (string name, Executor executor) throws Error {
  69. // Validate tool name according to MCP specification
  70. validate_tool_name (name);
  71. if (executors.contains (name)) {
  72. throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool already registered: %s".printf (name));
  73. }
  74. // Validate executor
  75. try {
  76. var definition = executor.get_definition ();
  77. if (definition.name != name) {
  78. throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool definition name mismatch: %s != %s".printf (definition.name, name));
  79. }
  80. // Validate tool definition name as well
  81. validate_tool_name (definition.name);
  82. // Validate input schema
  83. validate_tool_schema (definition.input_schema);
  84. executors.insert (name, executor);
  85. list_changed ();
  86. } catch (Error e) {
  87. throw new Mcp.Core.McpError.INVALID_PARAMS ("Failed to register tool %s: %s".printf (name, e.message));
  88. }
  89. }
  90. /**
  91. * Unregisters a tool executor.
  92. *
  93. * @param name The tool name to unregister
  94. * @return True if the tool was found and removed
  95. */
  96. public bool unregister_executor (string name) {
  97. bool removed = executors.remove (name);
  98. if (removed) {
  99. list_changed ();
  100. }
  101. return removed;
  102. }
  103. /**
  104. * Gets a registered tool executor.
  105. *
  106. * @param name The tool name
  107. * @return The executor or null if not found
  108. */
  109. public Executor? get_executor (string name) {
  110. return executors.lookup (name);
  111. }
  112. /**
  113. * Lists all registered tool names.
  114. *
  115. * @return Array of tool names
  116. */
  117. public string[] list_tools () {
  118. var names = new string[executors.size ()];
  119. int i = 0;
  120. foreach (var name in executors.get_keys ()) {
  121. names[i++] = name;
  122. }
  123. return names;
  124. }
  125. /**
  126. * Validates tool arguments against the tool's schema.
  127. *
  128. * @param tool_name The name of the tool
  129. * @param arguments The arguments to validate
  130. * @throws Error If validation fails
  131. */
  132. public void validate_arguments (string tool_name, Variant arguments) throws Error {
  133. var executor = executors.lookup (tool_name);
  134. if (executor == null) {
  135. throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool not found: %s".printf (tool_name));
  136. }
  137. var definition = executor.get_definition ();
  138. validate_against_schema (arguments, definition.input_schema);
  139. }
  140. /**
  141. * Handles the tools/list JSON-RPC method with complete pagination support.
  142. *
  143. * This method returns a paginated list of all available tools with their definitions.
  144. * Each tool includes its name, description, and input schema.
  145. *
  146. * @param params The method parameters (may include cursor for pagination)
  147. * @return The response result containing tools array and pagination info
  148. * @throws Error If handling fails
  149. */
  150. public async Variant handle_list (Variant @params) throws Error {
  151. // Extract cursor parameter if present
  152. string? cursor = null;
  153. if (@params.lookup_value ("cursor", null) != null) {
  154. cursor = @params.lookup_value ("cursor", VariantType.STRING).get_string ();
  155. }
  156. // Get all tool definitions from all executors
  157. var all_tools = new Gee.ArrayList<Mcp.Tools.Types.ToolDefinition> ();
  158. foreach (var name in executors.get_keys ()) {
  159. var executor = executors.lookup (name);
  160. try {
  161. var definition = executor.get_definition ();
  162. all_tools.add (definition);
  163. } catch (Error e) {
  164. // Continue with other executors
  165. }
  166. }
  167. // Sort tools by name for consistent pagination
  168. all_tools.sort ((a, b) => {
  169. return strcmp (a.name, b.name);
  170. });
  171. // Pagination settings
  172. const int PAGE_SIZE = 20; // Tools per page
  173. int start_index = 0;
  174. // Parse cursor to determine starting position
  175. if (cursor != null) {
  176. // Validate cursor format - should be a base64-encoded position
  177. try {
  178. // Decode base64 cursor to get position
  179. uint8[] cursor_bytes = Base64.decode (cursor);
  180. if (cursor_bytes.length == 4) {
  181. // Convert 4 bytes to int (little-endian)
  182. start_index = cursor_bytes[0] |
  183. (cursor_bytes[1] << 8) |
  184. (cursor_bytes[2] << 16) |
  185. (cursor_bytes[3] << 24);
  186. } else {
  187. // Fallback to simple integer parsing for backward compatibility
  188. start_index = int.parse (cursor);
  189. }
  190. } catch (Error e) {
  191. // Invalid cursor, start from beginning
  192. start_index = 0;
  193. }
  194. // Validate start_index bounds
  195. if (start_index < 0) {
  196. start_index = 0;
  197. } else if (start_index >= all_tools.size) {
  198. // Cursor beyond available tools, return empty result
  199. var result_builder = Mcp.Types.VariantUtils.new_dict_builder ();
  200. var tools_builder = Mcp.Types.VariantUtils.new_dict_array_builder ();
  201. result_builder.add ("{sv}", "tools", tools_builder.end ());
  202. return result_builder.end ();
  203. }
  204. }
  205. // Calculate the end index for this page
  206. int end_index = int.min (start_index + PAGE_SIZE, all_tools.size);
  207. // Get the tools for this page
  208. var page_tools = new Gee.ArrayList<Mcp.Tools.Types.ToolDefinition> ();
  209. for (int i = start_index; i < end_index; i++) {
  210. page_tools.add (all_tools[i]);
  211. }
  212. // Build response as Variant using utility functions
  213. var result_builder = Mcp.Types.VariantUtils.new_dict_builder ();
  214. // Serialize tools array
  215. var tools_builder = Mcp.Types.VariantUtils.new_dict_array_builder ();
  216. foreach (var tool in page_tools) {
  217. tools_builder.add_value (tool.to_variant ());
  218. }
  219. result_builder.add ("{sv}", "tools", tools_builder.end ());
  220. // Add nextCursor if there are more tools
  221. if (end_index < all_tools.size) {
  222. // Encode next position as base64 for proper cursor implementation
  223. uint8[] cursor_bytes = new uint8[4];
  224. int next_position = end_index;
  225. cursor_bytes[0] = (uint8) (next_position & 0xFF);
  226. cursor_bytes[1] = (uint8) ((next_position >> 8) & 0xFF);
  227. cursor_bytes[2] = (uint8) ((next_position >> 16) & 0xFF);
  228. cursor_bytes[3] = (uint8) ((next_position >> 24) & 0xFF);
  229. string next_cursor = Base64.encode (cursor_bytes);
  230. result_builder.add ("{sv}", "nextCursor", new Variant.string (next_cursor));
  231. }
  232. return result_builder.end ();
  233. }
  234. /**
  235. * Handles the tools/call JSON-RPC method.
  236. *
  237. * This method executes a tool with the provided arguments after validation.
  238. * It supports both synchronous and asynchronous tool execution with progress
  239. * reporting and cancellation support.
  240. *
  241. * @param params The method parameters containing tool name and arguments
  242. * @return The response result containing tool execution output
  243. * @throws Error If handling fails
  244. */
  245. public async Variant handle_call (Variant @params) throws Error {
  246. if (@params.lookup_value ("name", null) == null) {
  247. throw new Mcp.Core.McpError.INVALID_PARAMS ("Missing name parameter");
  248. }
  249. string name = @params.lookup_value ("name", VariantType.STRING).get_string ();
  250. Variant? arguments = null;
  251. if (@params.lookup_value ("arguments", null) != null) {
  252. arguments = @params.lookup_value ("arguments", VariantType.VARDICT);
  253. } else {
  254. // Create empty arguments object if not provided using utility function
  255. arguments = Mcp.Types.VariantUtils.new_empty_dict ();
  256. }
  257. // Check for progress token
  258. string? progress_token = null;
  259. if (@params.lookup_value ("_meta", null) != null) {
  260. var meta = @params.lookup_value ("_meta", VariantType.VARDICT);
  261. if (meta != null && meta.lookup_value ("progressToken", null) != null) {
  262. progress_token = meta.lookup_value ("progressToken", VariantType.STRING).get_string ();
  263. }
  264. }
  265. Executor? executor = executors.lookup (name);
  266. if (executor == null) {
  267. throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool not found: %s".printf (name));
  268. }
  269. // Validate arguments against tool schema
  270. validate_arguments (name, arguments);
  271. // Create execution context
  272. string execution_id = generate_execution_id ();
  273. var context = new Mcp.Tools.Types.ToolExecutionContext (
  274. execution_id, name, arguments, progress_token
  275. );
  276. active_executions.insert (execution_id, context);
  277. // Register progress token if provided
  278. if (progress_token != null) {
  279. progress_tokens.insert (progress_token, context.get_cancellable ());
  280. }
  281. try {
  282. // Mark execution as started
  283. context.mark_started ();
  284. // Execute tool with context support
  285. var result = yield execute_with_context (executor, context);
  286. // Mark execution as completed
  287. context.mark_completed ();
  288. // Clean up
  289. active_executions.remove (execution_id);
  290. if (progress_token != null) {
  291. progress_tokens.remove (progress_token);
  292. }
  293. return result.to_variant ();
  294. } catch (Error e) {
  295. // Mark execution as failed
  296. context.mark_failed ();
  297. // Clean up
  298. active_executions.remove (execution_id);
  299. if (progress_token != null) {
  300. progress_tokens.remove (progress_token);
  301. }
  302. // Create error result
  303. var error_content = new Mcp.Types.Common.TextContent (
  304. "Tool execution failed: %s".printf (e.message)
  305. );
  306. var error_result = new Mcp.Tools.Types.CallToolResult ();
  307. error_result.content.add (error_content);
  308. error_result.is_error = true;
  309. return error_result.to_variant ();
  310. }
  311. }
  312. /**
  313. * Executes a tool with context and timeout support.
  314. *
  315. * @param executor The tool executor
  316. * @param context The execution context
  317. * @return The execution result
  318. * @throws Error If execution fails or times out
  319. */
  320. private async Mcp.Tools.Types.CallToolResult execute_with_context (Executor executor, Mcp.Tools.Types.ToolExecutionContext context) throws Error {
  321. // Get timeout from tool definition or use default
  322. int timeout_seconds = 30;
  323. var definition = executor.get_definition ();
  324. if (definition.execution != null && definition.execution.timeout_seconds > 0) {
  325. timeout_seconds = definition.execution.timeout_seconds;
  326. }
  327. // Create timeout source
  328. var source = new TimeoutSource (timeout_seconds * 1000);
  329. bool timed_out = false;
  330. source.set_callback (() => {
  331. timed_out = true;
  332. context.cancel ();
  333. return false; // Remove timeout source
  334. });
  335. source.attach (null);
  336. try {
  337. // Execute tool with cancellable support
  338. var result = yield executor.execute_with_context (context.arguments, context.get_cancellable ());
  339. source.destroy ();
  340. if (timed_out) {
  341. throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool execution timed out after %d seconds".printf (timeout_seconds));
  342. }
  343. if (context.is_cancelled) {
  344. throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool execution was cancelled");
  345. }
  346. return result;
  347. } catch (Error e) {
  348. source.destroy ();
  349. // Check if error is due to cancellation
  350. if (e is IOError.CANCELLED || context.is_cancelled) {
  351. throw new Mcp.Core.McpError.TOOL_EXECUTION_FAILED ("Tool execution was cancelled");
  352. }
  353. throw e;
  354. }
  355. }
  356. /**
  357. * Generates a unique execution ID.
  358. *
  359. * @return A unique execution ID
  360. */
  361. private string generate_execution_id () {
  362. var timestamp = new DateTime.now_utc ().to_unix ();
  363. var random = GLib.Random.next_int ();
  364. return "exec_%lld_%u".printf (timestamp, random);
  365. }
  366. /**
  367. * Reports progress for a tool execution.
  368. *
  369. * @param progress_token The progress token
  370. * @param progress Current progress value (0-100)
  371. * @param message Optional progress message
  372. * @throws Error If the progress token is not found
  373. */
  374. public void report_progress (string progress_token, double progress, string? message = null) throws Error {
  375. if (progress_tokens.contains (progress_token)) {
  376. progress_updated (progress_token, progress, message);
  377. } else {
  378. throw new Mcp.Core.McpError.INVALID_PARAMS ("Invalid progress token: %s".printf (progress_token));
  379. }
  380. }
  381. /**
  382. * Cancels a tool execution using its progress token.
  383. *
  384. * @param progress_token The progress token of the execution to cancel
  385. * @throws Error If the progress token is not found
  386. */
  387. public void cancel_execution (string progress_token) throws Error {
  388. if (progress_tokens.contains (progress_token)) {
  389. var cancellable = progress_tokens.lookup (progress_token);
  390. cancellable.cancel ();
  391. } else {
  392. throw new Mcp.Core.McpError.INVALID_PARAMS ("Invalid progress token: %s".printf (progress_token));
  393. }
  394. }
  395. /**
  396. * Gets the execution context for a given execution ID.
  397. *
  398. * @param execution_id The execution ID
  399. * @return The execution context or null if not found
  400. */
  401. public Mcp.Tools.Types.ToolExecutionContext? get_execution_context (string execution_id) {
  402. return active_executions.lookup (execution_id);
  403. }
  404. /**
  405. * Gets all active execution contexts.
  406. *
  407. * @return Array of active execution contexts
  408. */
  409. public Mcp.Tools.Types.ToolExecutionContext[] get_active_executions () {
  410. var contexts = new Mcp.Tools.Types.ToolExecutionContext[active_executions.size ()];
  411. int i = 0;
  412. foreach (var context in active_executions.get_values ()) {
  413. contexts[i++] = context;
  414. }
  415. return contexts;
  416. }
  417. /**
  418. * Validates a tool name according to MCP specification.
  419. *
  420. * @param name The tool name to validate
  421. * @throws Error If name is invalid
  422. */
  423. private void validate_tool_name (string name) throws Error {
  424. // Check if name is null or empty
  425. if (name == null || name.strip () == "") {
  426. throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool name cannot be null or empty");
  427. }
  428. // Check length (1-128 characters)
  429. if (name.length < 1 || name.length > 128) {
  430. throw new Mcp.Core.McpError.INVALID_PARAMS (
  431. "Tool name must be between 1 and 128 characters long, got %d characters".printf (name.length)
  432. );
  433. }
  434. // Check for allowed characters: ASCII letters, digits, underscore, hyphen, and dot
  435. // Regex pattern: ^[a-zA-Z0-9_.-]+$
  436. try {
  437. Regex name_regex = new Regex ("^[a-zA-Z0-9_.-]+$");
  438. if (!name_regex.match (name)) {
  439. throw new Mcp.Core.McpError.INVALID_PARAMS (
  440. "Tool name contains invalid characters. Only ASCII letters, digits, underscore, hyphen, and dot are allowed"
  441. );
  442. }
  443. } catch (Error e) {
  444. throw new Mcp.Core.McpError.INVALID_PARAMS (
  445. "Failed to validate tool name: %s".printf (e.message)
  446. );
  447. }
  448. // Check for spaces, commas, or other special characters (additional safety check)
  449. foreach (char c in name.to_utf8 ()) {
  450. if (c == ' ' || c == ',' || c == ';' || c == ':' || c == '/' || c == '\\') {
  451. throw new Mcp.Core.McpError.INVALID_PARAMS (
  452. "Tool name contains invalid character '%c'. Spaces, commas, and other special characters are not allowed".printf (c)
  453. );
  454. }
  455. }
  456. }
  457. /**
  458. * Sets up the basic JSON schema validation.
  459. */
  460. private void setup_validation_schema () {
  461. // This would set up a basic schema for validation
  462. // For now, we'll use simple validation methods
  463. }
  464. /**
  465. * Validates a tool's JSON schema with comprehensive checks.
  466. *
  467. * @param schema The schema to validate
  468. * @throws Error If the schema is invalid
  469. */
  470. private void validate_tool_schema (Variant schema) throws Error {
  471. if (!schema.is_of_type (VariantType.VARDICT)) {
  472. throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool schema must be a dictionary");
  473. }
  474. if (schema.lookup_value ("type", null) == null) {
  475. throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool schema must have a 'type' property");
  476. }
  477. string schema_type = schema.lookup_value ("type", VariantType.STRING).get_string ();
  478. if (schema_type != "object") {
  479. throw new Mcp.Core.McpError.INVALID_PARAMS ("Tool schema type must be 'object'");
  480. }
  481. // Validate properties if present
  482. if (schema.lookup_value ("properties", null) != null) {
  483. var properties = schema.lookup_value ("properties", VariantType.VARDICT);
  484. if (!properties.is_of_type (VariantType.VARDICT)) {
  485. throw new Mcp.Core.McpError.INVALID_PARAMS ("Schema properties must be a dictionary");
  486. }
  487. // Validate each property schema
  488. validate_property_schemas (properties);
  489. }
  490. // Validate required array if present
  491. if (schema.lookup_value ("required", null) != null) {
  492. var required = schema.lookup_value ("required", new VariantType ("as"));
  493. if (!required.is_of_type (new VariantType ("as"))) {
  494. throw new Mcp.Core.McpError.INVALID_PARAMS ("Schema required must be an array of strings");
  495. }
  496. }
  497. }
  498. /**
  499. * Validates property schemas within a schema.
  500. *
  501. * @param properties The properties dictionary to validate
  502. * @throws Error If any property schema is invalid
  503. */
  504. private void validate_property_schemas (Variant properties) throws Error {
  505. var iter = properties.iterator ();
  506. string? prop_name;
  507. Variant? prop_schema;
  508. while (iter.next ("{sv}", out prop_name, out prop_schema)) {
  509. if (prop_name == null || prop_schema == null) {
  510. continue;
  511. }
  512. if (!prop_schema.is_of_type (VariantType.VARDICT)) {
  513. throw new Mcp.Core.McpError.INVALID_PARAMS (
  514. "Property schema for '%s' must be a dictionary".printf (prop_name)
  515. );
  516. }
  517. // Check for type property
  518. if (prop_schema.lookup_value ("type", null) == null) {
  519. throw new Mcp.Core.McpError.INVALID_PARAMS (
  520. "Property schema for '%s' must have a 'type' property".printf (prop_name)
  521. );
  522. }
  523. // Validate type-specific constraints
  524. validate_property_constraints (prop_name, prop_schema);
  525. }
  526. }
  527. /**
  528. * Validates type-specific constraints for a property.
  529. *
  530. * @param prop_name The property name
  531. * @param prop_schema The property schema
  532. * @throws Error If constraints are invalid
  533. */
  534. private void validate_property_constraints (string prop_name, Variant prop_schema) throws Error {
  535. string prop_type = prop_schema.lookup_value ("type", VariantType.STRING).get_string ();
  536. switch (prop_type) {
  537. case "string":
  538. validate_string_constraints (prop_name, prop_schema);
  539. break;
  540. case "number":
  541. case "integer":
  542. validate_numeric_constraints (prop_name, prop_schema);
  543. break;
  544. case "array":
  545. validate_array_constraints (prop_name, prop_schema);
  546. break;
  547. case "object":
  548. validate_object_constraints (prop_name, prop_schema);
  549. break;
  550. case "boolean":
  551. // Boolean type has no additional constraints
  552. break;
  553. default:
  554. throw new Mcp.Core.McpError.INVALID_PARAMS (
  555. "Invalid property type '%s' for property '%s'".printf (prop_type, prop_name)
  556. );
  557. }
  558. }
  559. /**
  560. * Validates string-specific constraints.
  561. *
  562. * @param prop_name The property name
  563. * @param prop_schema The property schema
  564. * @throws Error If constraints are invalid
  565. */
  566. private void validate_string_constraints (string prop_name, Variant prop_schema) throws Error {
  567. // Check minLength if present
  568. if (prop_schema.lookup_value ("minLength", null) != null) {
  569. var min_length = prop_schema.lookup_value ("minLength", VariantType.INT32);
  570. if (!min_length.is_of_type (VariantType.INT32) || min_length.get_int32 () < 0) {
  571. throw new Mcp.Core.McpError.INVALID_PARAMS (
  572. "Invalid minLength for property '%s': must be a non-negative integer".printf (prop_name)
  573. );
  574. }
  575. }
  576. // Check maxLength if present
  577. if (prop_schema.lookup_value ("maxLength", null) != null) {
  578. var max_length = prop_schema.lookup_value ("maxLength", VariantType.INT32);
  579. if (!max_length.is_of_type (VariantType.INT32) || max_length.get_int32 () < 0) {
  580. throw new Mcp.Core.McpError.INVALID_PARAMS (
  581. "Invalid maxLength for property '%s': must be a non-negative integer".printf (prop_name)
  582. );
  583. }
  584. }
  585. // Check pattern if present
  586. if (prop_schema.lookup_value ("pattern", null) != null) {
  587. var pattern = prop_schema.lookup_value ("pattern", VariantType.STRING);
  588. if (!pattern.is_of_type (VariantType.STRING)) {
  589. throw new Mcp.Core.McpError.INVALID_PARAMS (
  590. "Invalid pattern for property '%s': must be a string".printf (prop_name)
  591. );
  592. }
  593. // Validate regex pattern (basic check)
  594. try {
  595. // Simple validation - try to compile the regex
  596. Regex regex = new Regex (pattern.get_string ());
  597. } catch (Error e) {
  598. throw new Mcp.Core.McpError.INVALID_PARAMS (
  599. "Invalid pattern for property '%s': %s".printf (prop_name, e.message)
  600. );
  601. }
  602. }
  603. // Check format if present
  604. if (prop_schema.lookup_value ("format", null) != null) {
  605. var format = prop_schema.lookup_value ("format", VariantType.STRING);
  606. if (!format.is_of_type (VariantType.STRING)) {
  607. throw new Mcp.Core.McpError.INVALID_PARAMS (
  608. "Invalid format for property '%s': must be a string".printf (prop_name)
  609. );
  610. }
  611. // Validate supported formats
  612. string format_str = format.get_string ();
  613. if (!is_supported_format (format_str)) {
  614. throw new Mcp.Core.McpError.INVALID_PARAMS (
  615. "Unsupported format '%s' for property '%s'".printf (format_str, prop_name)
  616. );
  617. }
  618. }
  619. }
  620. /**
  621. * Validates numeric-specific constraints.
  622. *
  623. * @param prop_name The property name
  624. * @param prop_schema The property schema
  625. * @throws Error If constraints are invalid
  626. */
  627. private void validate_numeric_constraints (string prop_name, Variant prop_schema) throws Error {
  628. // Check minimum if present
  629. if (prop_schema.lookup_value ("minimum", null) != null) {
  630. var minimum = prop_schema.lookup_value ("minimum", VariantType.DOUBLE);
  631. if (!minimum.is_of_type (VariantType.DOUBLE)) {
  632. throw new Mcp.Core.McpError.INVALID_PARAMS (
  633. "Invalid minimum for property '%s': must be a number".printf (prop_name)
  634. );
  635. }
  636. }
  637. // Check maximum if present
  638. if (prop_schema.lookup_value ("maximum", null) != null) {
  639. var maximum = prop_schema.lookup_value ("maximum", VariantType.DOUBLE);
  640. if (!maximum.is_of_type (VariantType.DOUBLE)) {
  641. throw new Mcp.Core.McpError.INVALID_PARAMS (
  642. "Invalid maximum for property '%s': must be a number".printf (prop_name)
  643. );
  644. }
  645. }
  646. // Check exclusiveMinimum if present
  647. if (prop_schema.lookup_value ("exclusiveMinimum", null) != null) {
  648. var excl_min = prop_schema.lookup_value ("exclusiveMinimum", VariantType.DOUBLE);
  649. if (!excl_min.is_of_type (VariantType.DOUBLE)) {
  650. throw new Mcp.Core.McpError.INVALID_PARAMS (
  651. "Invalid exclusiveMinimum for property '%s': must be a number".printf (prop_name)
  652. );
  653. }
  654. }
  655. // Check exclusiveMaximum if present
  656. if (prop_schema.lookup_value ("exclusiveMaximum", null) != null) {
  657. var excl_max = prop_schema.lookup_value ("exclusiveMaximum", VariantType.DOUBLE);
  658. if (!excl_max.is_of_type (VariantType.DOUBLE)) {
  659. throw new Mcp.Core.McpError.INVALID_PARAMS (
  660. "Invalid exclusiveMaximum for property '%s': must be a number".printf (prop_name)
  661. );
  662. }
  663. }
  664. // Check multipleOf if present
  665. if (prop_schema.lookup_value ("multipleOf", null) != null) {
  666. var multiple_of = prop_schema.lookup_value ("multipleOf", VariantType.DOUBLE);
  667. if (!multiple_of.is_of_type (VariantType.DOUBLE) || multiple_of.get_double () <= 0) {
  668. throw new Mcp.Core.McpError.INVALID_PARAMS (
  669. "Invalid multipleOf for property '%s': must be a positive number".printf (prop_name)
  670. );
  671. }
  672. }
  673. }
  674. /**
  675. * Validates array-specific constraints.
  676. *
  677. * @param prop_name The property name
  678. * @param prop_schema The property schema
  679. * @throws Error If constraints are invalid
  680. */
  681. private void validate_array_constraints (string prop_name, Variant prop_schema) throws Error {
  682. // Check minItems if present
  683. if (prop_schema.lookup_value ("minItems", null) != null) {
  684. var min_items = prop_schema.lookup_value ("minItems", VariantType.INT32);
  685. if (!min_items.is_of_type (VariantType.INT32) || min_items.get_int32 () < 0) {
  686. throw new Mcp.Core.McpError.INVALID_PARAMS (
  687. "Invalid minItems for property '%s': must be a non-negative integer".printf (prop_name)
  688. );
  689. }
  690. }
  691. // Check maxItems if present
  692. if (prop_schema.lookup_value ("maxItems", null) != null) {
  693. var max_items = prop_schema.lookup_value ("maxItems", VariantType.INT32);
  694. if (!max_items.is_of_type (VariantType.INT32) || max_items.get_int32 () < 0) {
  695. throw new Mcp.Core.McpError.INVALID_PARAMS (
  696. "Invalid maxItems for property '%s': must be a non-negative integer".printf (prop_name)
  697. );
  698. }
  699. }
  700. // Check items schema if present
  701. if (prop_schema.lookup_value ("items", null) != null) {
  702. var items = prop_schema.lookup_value ("items", VariantType.VARDICT);
  703. if (!items.is_of_type (VariantType.VARDICT)) {
  704. throw new Mcp.Core.McpError.INVALID_PARAMS (
  705. "Invalid items for property '%s': must be a schema object".printf (prop_name)
  706. );
  707. }
  708. // Recursively validate items schema
  709. validate_property_constraints (prop_name + "[]", items);
  710. }
  711. }
  712. /**
  713. * Validates object-specific constraints.
  714. *
  715. * @param prop_name The property name
  716. * @param prop_schema The property schema
  717. * @throws Error If constraints are invalid
  718. */
  719. private void validate_object_constraints (string prop_name, Variant prop_schema) throws Error {
  720. // Check properties if present
  721. if (prop_schema.lookup_value ("properties", null) != null) {
  722. var properties = prop_schema.lookup_value ("properties", VariantType.VARDICT);
  723. if (!properties.is_of_type (VariantType.VARDICT)) {
  724. throw new Mcp.Core.McpError.INVALID_PARAMS (
  725. "Invalid properties for property '%s': must be a dictionary".printf (prop_name)
  726. );
  727. }
  728. // Recursively validate property schemas
  729. validate_property_schemas (properties);
  730. }
  731. // Check required if present
  732. if (prop_schema.lookup_value ("required", null) != null) {
  733. var required = prop_schema.lookup_value ("required", new VariantType ("as"));
  734. if (!required.is_of_type (new VariantType ("as"))) {
  735. throw new Mcp.Core.McpError.INVALID_PARAMS (
  736. "Invalid required for property '%s': must be an array of strings".printf (prop_name)
  737. );
  738. }
  739. }
  740. }
  741. /**
  742. * Checks if a format string is supported.
  743. *
  744. * @param format The format string to check
  745. * @return True if the format is supported
  746. */
  747. private bool is_supported_format (string format) {
  748. // List of supported JSON Schema formats
  749. string[] supported_formats = {
  750. "date-time", "date", "time", "email", "hostname", "ipv4", "ipv6", "uri", "uuid"
  751. };
  752. foreach (var supported in supported_formats) {
  753. if (format == supported) {
  754. return true;
  755. }
  756. }
  757. return false;
  758. }
  759. /**
  760. * Validates arguments against a JSON schema with comprehensive checks.
  761. *
  762. * @param arguments The arguments to validate
  763. * @param schema The schema to validate against
  764. * @throws Error If validation fails
  765. */
  766. private void validate_against_schema (Variant arguments, Variant schema) throws Error {
  767. if (!schema.is_of_type (VariantType.VARDICT)) {
  768. return; // Invalid schema format, skip validation
  769. }
  770. if (!arguments.is_of_type (VariantType.VARDICT)) {
  771. throw new Mcp.Core.McpError.INVALID_PARAMS ("Arguments must be a dictionary");
  772. }
  773. // Check required properties
  774. if (schema.lookup_value ("required", null) != null) {
  775. var required = schema.lookup_value ("required", new VariantType ("as"));
  776. if (required.is_of_type (new VariantType ("as"))) {
  777. var required_array = required.get_strv ();
  778. for (int i = 0; i < required_array.length; i++) {
  779. var required_prop = required_array[i];
  780. if (arguments.lookup_value (required_prop, null) == null) {
  781. throw new Mcp.Core.McpError.INVALID_PARAMS (
  782. "Missing required property: %s".printf (required_prop)
  783. );
  784. }
  785. }
  786. }
  787. }
  788. // Validate each argument against its schema
  789. if (schema.lookup_value ("properties", null) != null) {
  790. var properties = schema.lookup_value ("properties", VariantType.VARDICT);
  791. if (properties.is_of_type (VariantType.VARDICT)) {
  792. validate_arguments_against_properties (arguments, properties);
  793. }
  794. }
  795. // Check for additional properties if allowed
  796. if (schema.lookup_value ("additionalProperties", null) != null) {
  797. var additional_props = schema.lookup_value ("additionalProperties", null);
  798. if (additional_props.is_of_type (VariantType.BOOLEAN) && !additional_props.get_boolean ()) {
  799. // No additional properties allowed, check for unexpected properties
  800. check_unexpected_properties (arguments, schema);
  801. }
  802. }
  803. }
  804. /**
  805. * Validates arguments against property schemas.
  806. *
  807. * @param arguments The arguments to validate
  808. * @param properties The property schemas
  809. * @throws Error If validation fails
  810. */
  811. private void validate_arguments_against_properties (Variant arguments, Variant properties) throws Error {
  812. var iter = properties.iterator ();
  813. string? prop_name;
  814. Variant? prop_schema;
  815. while (iter.next ("{sv}", out prop_name, out prop_schema)) {
  816. if (prop_name == null || prop_schema == null) {
  817. continue;
  818. }
  819. // Check if property is present in arguments
  820. var arg_value = arguments.lookup_value (prop_name, null);
  821. if (arg_value != null) {
  822. // Validate the argument value against the property schema
  823. validate_value_against_schema (arg_value, prop_schema, prop_name);
  824. }
  825. }
  826. }
  827. /**
  828. * Validates a value against a property schema.
  829. *
  830. * @param value The value to validate
  831. * @param schema The schema to validate against
  832. * @param prop_name The property name for error messages
  833. * @throws Error If validation fails
  834. */
  835. private void validate_value_against_schema (Variant value, Variant schema, string prop_name) throws Error {
  836. if (!schema.is_of_type (VariantType.VARDICT)) {
  837. return; // Invalid schema, skip validation
  838. }
  839. if (schema.lookup_value ("type", null) == null) {
  840. return; // No type specified, skip validation
  841. }
  842. string expected_type = schema.lookup_value ("type", VariantType.STRING).get_string ();
  843. // Check type compatibility
  844. if (!is_type_compatible (value, expected_type)) {
  845. throw new Mcp.Core.McpError.INVALID_PARAMS (
  846. "Property '%s' has invalid type: expected %s".printf (prop_name, expected_type)
  847. );
  848. }
  849. // Perform type-specific validation
  850. switch (expected_type) {
  851. case "string":
  852. validate_string_value (value, schema, prop_name);
  853. break;
  854. case "number":
  855. case "integer":
  856. validate_numeric_value (value, schema, prop_name);
  857. break;
  858. case "array":
  859. validate_array_value (value, schema, prop_name);
  860. break;
  861. case "object":
  862. validate_object_value (value, schema, prop_name);
  863. break;
  864. case "boolean":
  865. // Boolean values don't need additional validation
  866. break;
  867. }
  868. }
  869. /**
  870. * Checks if a Variant value is compatible with an expected JSON Schema type.
  871. *
  872. * @param value The value to check
  873. * @param expected_type The expected type
  874. * @return True if compatible
  875. */
  876. private bool is_type_compatible (Variant value, string expected_type) {
  877. switch (expected_type) {
  878. case "string":
  879. return value.is_of_type (VariantType.STRING);
  880. case "number":
  881. return value.is_of_type (VariantType.DOUBLE) || value.is_of_type (VariantType.INT32) ||
  882. value.is_of_type (VariantType.INT64) || value.is_of_type (VariantType.UINT32) ||
  883. value.is_of_type (VariantType.UINT64);
  884. case "integer":
  885. return value.is_of_type (VariantType.INT32) || value.is_of_type (VariantType.INT64) ||
  886. value.is_of_type (VariantType.UINT32) || value.is_of_type (VariantType.UINT64);
  887. case "boolean":
  888. return value.is_of_type (VariantType.BOOLEAN);
  889. case "array":
  890. return value.is_of_type (new VariantType ("av")) || value.is_of_type (new VariantType ("aa{sv}"));
  891. case "object":
  892. return value.is_of_type (VariantType.VARDICT);
  893. default:
  894. return false;
  895. }
  896. }
  897. /**
  898. * Validates a string value against string constraints.
  899. *
  900. * @param value The value to validate
  901. * @param schema The schema to validate against
  902. * @param prop_name The property name
  903. * @throws Error If validation fails
  904. */
  905. private void validate_string_value (Variant value, Variant schema, string prop_name) throws Error {
  906. string str_value = value.get_string ();
  907. // Check minLength
  908. if (schema.lookup_value ("minLength", null) != null) {
  909. int min_length = schema.lookup_value ("minLength", VariantType.INT32).get_int32 ();
  910. if (str_value.length < min_length) {
  911. throw new Mcp.Core.McpError.INVALID_PARAMS (
  912. "Property '%s' is too short: minimum length is %d".printf (prop_name, min_length)
  913. );
  914. }
  915. }
  916. // Check maxLength
  917. if (schema.lookup_value ("maxLength", null) != null) {
  918. int max_length = schema.lookup_value ("maxLength", VariantType.INT32).get_int32 ();
  919. if (str_value.length > max_length) {
  920. throw new Mcp.Core.McpError.INVALID_PARAMS (
  921. "Property '%s' is too long: maximum length is %d".printf (prop_name, max_length)
  922. );
  923. }
  924. }
  925. // Check pattern
  926. if (schema.lookup_value ("pattern", null) != null) {
  927. string pattern = schema.lookup_value ("pattern", VariantType.STRING).get_string ();
  928. try {
  929. Regex regex = new Regex (pattern);
  930. if (!regex.match (str_value)) {
  931. throw new Mcp.Core.McpError.INVALID_PARAMS (
  932. "Property '%s' does not match required pattern".printf (prop_name)
  933. );
  934. }
  935. } catch (Error e) {
  936. throw new Mcp.Core.McpError.INVALID_PARAMS (
  937. "Invalid pattern for property '%s': %s".printf (prop_name, e.message)
  938. );
  939. }
  940. }
  941. // Check format
  942. if (schema.lookup_value ("format", null) != null) {
  943. string format = schema.lookup_value ("format", VariantType.STRING).get_string ();
  944. if (!validate_format (str_value, format)) {
  945. throw new Mcp.Core.McpError.INVALID_PARAMS (
  946. "Property '%s' does not match required format: %s".printf (prop_name, format)
  947. );
  948. }
  949. }
  950. }
  951. /**
  952. * Validates a numeric value against numeric constraints.
  953. *
  954. * @param value The value to validate
  955. * @param schema The schema to validate against
  956. * @param prop_name The property name
  957. * @throws Error If validation fails
  958. */
  959. private void validate_numeric_value (Variant value, Variant schema, string prop_name) throws Error {
  960. double num_value;
  961. if (value.is_of_type (VariantType.DOUBLE)) {
  962. num_value = value.get_double ();
  963. } else if (value.is_of_type (VariantType.INT32)) {
  964. num_value = (double) value.get_int32 ();
  965. } else if (value.is_of_type (VariantType.INT64)) {
  966. num_value = (double) value.get_int64 ();
  967. } else if (value.is_of_type (VariantType.UINT32)) {
  968. num_value = (double) value.get_uint32 ();
  969. } else if (value.is_of_type (VariantType.UINT64)) {
  970. num_value = (double) value.get_uint64 ();
  971. } else {
  972. throw new Mcp.Core.McpError.INVALID_PARAMS (
  973. "Property '%s' has invalid numeric type".printf (prop_name)
  974. );
  975. }
  976. // Check minimum
  977. if (schema.lookup_value ("minimum", null) != null) {
  978. double minimum = schema.lookup_value ("minimum", VariantType.DOUBLE).get_double ();
  979. if (num_value < minimum) {
  980. throw new Mcp.Core.McpError.INVALID_PARAMS (
  981. "Property '%s' is too small: minimum is %g".printf (prop_name, minimum)
  982. );
  983. }
  984. }
  985. // Check maximum
  986. if (schema.lookup_value ("maximum", null) != null) {
  987. double maximum = schema.lookup_value ("maximum", VariantType.DOUBLE).get_double ();
  988. if (num_value > maximum) {
  989. throw new Mcp.Core.McpError.INVALID_PARAMS (
  990. "Property '%s' is too large: maximum is %g".printf (prop_name, maximum)
  991. );
  992. }
  993. }
  994. // Check exclusiveMinimum
  995. if (schema.lookup_value ("exclusiveMinimum", null) != null) {
  996. double excl_min = schema.lookup_value ("exclusiveMinimum", VariantType.DOUBLE).get_double ();
  997. if (num_value <= excl_min) {
  998. throw new Mcp.Core.McpError.INVALID_PARAMS (
  999. "Property '%s' must be greater than %g".printf (prop_name, excl_min)
  1000. );
  1001. }
  1002. }
  1003. // Check exclusiveMaximum
  1004. if (schema.lookup_value ("exclusiveMaximum", null) != null) {
  1005. double excl_max = schema.lookup_value ("exclusiveMaximum", VariantType.DOUBLE).get_double ();
  1006. if (num_value >= excl_max) {
  1007. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1008. "Property '%s' must be less than %g".printf (prop_name, excl_max)
  1009. );
  1010. }
  1011. }
  1012. // Check multipleOf
  1013. if (schema.lookup_value ("multipleOf", null) != null) {
  1014. double multiple_of = schema.lookup_value ("multipleOf", VariantType.DOUBLE).get_double ();
  1015. if (multiple_of > 0) {
  1016. double remainder = num_value % multiple_of;
  1017. if (remainder > 1e-10 && remainder < (multiple_of - 1e-10)) {
  1018. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1019. "Property '%s' must be a multiple of %g".printf (prop_name, multiple_of)
  1020. );
  1021. }
  1022. }
  1023. }
  1024. // Check integer constraint
  1025. if (schema.lookup_value ("type", VariantType.STRING).get_string () == "integer") {
  1026. if (num_value != Math.floor (num_value)) {
  1027. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1028. "Property '%s' must be an integer".printf (prop_name)
  1029. );
  1030. }
  1031. }
  1032. }
  1033. /**
  1034. * Validates an array value against array constraints.
  1035. *
  1036. * @param value The value to validate
  1037. * @param schema The schema to validate against
  1038. * @param prop_name The property name
  1039. * @throws Error If validation fails
  1040. */
  1041. private void validate_array_value (Variant value, Variant schema, string prop_name) throws Error {
  1042. Variant array_value;
  1043. if (value.is_of_type (new VariantType ("av"))) {
  1044. array_value = value;
  1045. } else if (value.is_of_type (new VariantType ("aa{sv}"))) {
  1046. array_value = value;
  1047. } else {
  1048. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1049. "Property '%s' has invalid array type".printf (prop_name)
  1050. );
  1051. }
  1052. size_t array_length = array_value.n_children ();
  1053. // Check minItems
  1054. if (schema.lookup_value ("minItems", null) != null) {
  1055. int min_items = schema.lookup_value ("minItems", VariantType.INT32).get_int32 ();
  1056. if (array_length < min_items) {
  1057. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1058. "Property '%s' has too few items: minimum is %d".printf (prop_name, min_items)
  1059. );
  1060. }
  1061. }
  1062. // Check maxItems
  1063. if (schema.lookup_value ("maxItems", null) != null) {
  1064. int max_items = schema.lookup_value ("maxItems", VariantType.INT32).get_int32 ();
  1065. if (array_length > max_items) {
  1066. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1067. "Property '%s' has too many items: maximum is %d".printf (prop_name, max_items)
  1068. );
  1069. }
  1070. }
  1071. // Validate items if schema is provided
  1072. if (schema.lookup_value ("items", null) != null) {
  1073. var items_schema = schema.lookup_value ("items", VariantType.VARDICT);
  1074. if (items_schema.is_of_type (VariantType.VARDICT)) {
  1075. for (size_t i = 0; i < array_length; i++) {
  1076. var item_value = array_value.get_child_value (i);
  1077. validate_value_against_schema (item_value, items_schema, "%s[%zu]".printf (prop_name, i));
  1078. }
  1079. }
  1080. }
  1081. }
  1082. /**
  1083. * Validates an object value against object constraints.
  1084. *
  1085. * @param value The value to validate
  1086. * @param schema The schema to validate against
  1087. * @param prop_name The property name
  1088. * @throws Error If validation fails
  1089. */
  1090. private void validate_object_value (Variant value, Variant schema, string prop_name) throws Error {
  1091. if (!value.is_of_type (VariantType.VARDICT)) {
  1092. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1093. "Property '%s' has invalid object type".printf (prop_name)
  1094. );
  1095. }
  1096. // Validate properties if schema is provided
  1097. if (schema.lookup_value ("properties", null) != null) {
  1098. var properties_schema = schema.lookup_value ("properties", VariantType.VARDICT);
  1099. if (properties_schema.is_of_type (VariantType.VARDICT)) {
  1100. validate_arguments_against_properties (value, properties_schema);
  1101. }
  1102. }
  1103. // Check required properties
  1104. if (schema.lookup_value ("required", null) != null) {
  1105. var required = schema.lookup_value ("required", new VariantType ("as"));
  1106. if (required.is_of_type (new VariantType ("as"))) {
  1107. var required_array = required.get_strv ();
  1108. for (int i = 0; i < required_array.length; i++) {
  1109. var required_prop = required_array[i];
  1110. if (value.lookup_value (required_prop, null) == null) {
  1111. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1112. "Property '%s' is missing required property: %s".printf (prop_name, required_prop)
  1113. );
  1114. }
  1115. }
  1116. }
  1117. }
  1118. }
  1119. /**
  1120. * Validates a string value against a format.
  1121. *
  1122. * @param value The value to validate
  1123. * @param format The format to validate against
  1124. * @return True if valid
  1125. */
  1126. private bool validate_format (string value, string format) {
  1127. switch (format) {
  1128. case "email":
  1129. // Basic email validation
  1130. return Regex.match_simple ("^[^@]+@[^@]+\\.[^@]+$", value);
  1131. case "uri":
  1132. // Basic URI validation
  1133. return Regex.match_simple ("^[a-zA-Z][a-zA-Z0-9+.-]*://", value);
  1134. case "uuid":
  1135. // UUID validation
  1136. 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);
  1137. case "ipv4":
  1138. // IPv4 validation
  1139. 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);
  1140. case "ipv6":
  1141. // IPv6 validation (basic)
  1142. return Regex.match_simple ("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$", value);
  1143. case "date-time":
  1144. // ISO 8601 date-time validation (basic)
  1145. return Regex.match_simple ("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", value);
  1146. case "date":
  1147. // ISO 8601 date validation (basic)
  1148. return Regex.match_simple ("^\\d{4}-\\d{2}-\\d{2}$", value);
  1149. case "time":
  1150. // ISO 8601 time validation (basic)
  1151. return Regex.match_simple ("^\\d{2}:\\d{2}:\\d{2}$", value);
  1152. case "hostname":
  1153. // Hostname validation (basic)
  1154. return Regex.match_simple ("^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?$", value);
  1155. default:
  1156. // Unknown format, assume valid
  1157. return true;
  1158. }
  1159. }
  1160. /**
  1161. * Checks for unexpected properties in arguments.
  1162. *
  1163. * @param arguments The arguments to check
  1164. * @param schema The schema defining allowed properties
  1165. * @throws Error If unexpected properties are found
  1166. */
  1167. private void check_unexpected_properties (Variant arguments, Variant schema) throws Error {
  1168. if (schema.lookup_value ("properties", null) == null) {
  1169. // No properties defined, any property is unexpected
  1170. var iter = arguments.iterator ();
  1171. string? prop_name;
  1172. Variant? prop_value;
  1173. while (iter.next ("{sv}", out prop_name, out prop_value)) {
  1174. if (prop_name != null) {
  1175. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1176. "Unexpected property: %s".printf (prop_name)
  1177. );
  1178. }
  1179. }
  1180. return;
  1181. }
  1182. var properties = schema.lookup_value ("properties", VariantType.VARDICT);
  1183. var allowed_props = new Gee.HashSet<string> ();
  1184. // Collect all allowed property names
  1185. var iter = properties.iterator ();
  1186. string? prop_name;
  1187. Variant? prop_schema;
  1188. while (iter.next ("{sv}", out prop_name, out prop_schema)) {
  1189. if (prop_name != null) {
  1190. allowed_props.add (prop_name);
  1191. }
  1192. }
  1193. // Check for unexpected properties
  1194. iter = arguments.iterator ();
  1195. Variant? prop_value;
  1196. while (iter.next ("{sv}", out prop_name, out prop_value)) {
  1197. if (prop_name != null && !allowed_props.contains (prop_name)) {
  1198. throw new Mcp.Core.McpError.INVALID_PARAMS (
  1199. "Unexpected property: %s".printf (prop_name)
  1200. );
  1201. }
  1202. }
  1203. }
  1204. }
  1205. }