/* * 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 */ /** * Prompt manager for MCP protocol. * * This class manages prompt templates and handles prompt-related * JSON-RPC method calls. */ namespace Mcp.Prompts { /** * Prompt manager implementation. */ public class Manager : GLib.Object { private HashTable templates; /** * Signal emitted when the prompt list changes. */ public signal void list_changed (); /** * Creates a new Manager. */ public Manager () { templates = new HashTable (str_hash, str_equal); } /** * Registers a prompt template. * * @param name The prompt name * @param template The template implementation * @throws Error If registration fails */ public void register_template (string name, Template template) throws Error { if (name == null || name.strip () == "") { throw new Mcp.Core.McpError.INVALID_PARAMS ("Prompt name cannot be empty or null. Please provide a valid name for the prompt template."); } if (templates.contains (name)) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Prompt '%s' is already registered. Use a different name or unregister the existing prompt first.".printf (name)); } templates.insert (name, template); list_changed (); } /** * Unregisters a prompt template. * * @param name The prompt name to unregister * @throws Error If unregistration fails */ public void unregister_template (string name) throws Error { if (!templates.contains (name)) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Prompt '%s' not found. Cannot unregister a prompt that doesn't exist. Check the name and try again.".printf (name)); } templates.remove (name); list_changed (); } /** * Gets a registered prompt template. * * @param name The prompt name * @return The template or null if not found */ public Template? get_template (string name) { return templates.lookup (name); } /** * Gets all registered prompt templates. * * @return A hash table of all templates */ public HashTable get_templates () { return templates; } /** * Lists all registered prompt names. * * @return A list of prompt names */ public Gee.ArrayList list_prompt_names () { var names = new Gee.ArrayList (); foreach (var name in templates.get_keys ()) { names.add (name); } return names; } /** * Checks if a prompt is registered. * * @param name The prompt name * @return True if the prompt is registered */ public bool has_prompt (string name) { return templates.contains (name); } /** * Handles the prompts/list JSON-RPC method. * * @param params The method parameters * @return The response result * @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 prompt definitions from all templates var all_prompts = new Gee.ArrayList (); foreach (var name in templates.get_keys ()) { var template = templates.lookup (name); try { var definition = template.get_definition (); all_prompts.add (definition); } catch (Error e) { // Continue with other templates } } // Sort prompts by name for consistent pagination all_prompts.sort ((a, b) => { return strcmp (a.name, b.name); }); // Pagination settings const int PAGE_SIZE = 10; int start_index = 0; // Parse cursor to determine starting position if (cursor != null) { // Simple cursor implementation: cursor is the page number as a string // Default to 0 if parsing fails start_index = int.parse (cursor) * PAGE_SIZE; if (start_index < 0) { start_index = 0; } } // Calculate the end index for this page int end_index = int.min (start_index + PAGE_SIZE, all_prompts.size); // Get the prompts for this page var page_prompts = new Gee.ArrayList (); for (int i = start_index; i < end_index; i++) { page_prompts.add (all_prompts[i]); } // Build response as Variant using utility functions var result_builder = Mcp.Types.VariantUtils.new_dict_builder (); // Serialize prompts array var prompts_builder = Mcp.Types.VariantUtils.new_dict_array_builder (); foreach (var prompt in page_prompts) { prompts_builder.add_value (prompt.to_variant ()); } result_builder.add ("{sv}", "prompts", prompts_builder.end ()); // Add nextCursor if there are more prompts if (end_index < all_prompts.size) { // Next cursor is the next page number int next_page = start_index / PAGE_SIZE + 1; result_builder.add ("{sv}", "nextCursor", new Variant.string (next_page.to_string ())); } return result_builder.end (); } /** * Handles the prompts/get JSON-RPC method. * * @param params The method parameters * @return The response result * @throws Error If handling fails */ public async Variant handle_get (Variant @params) throws Error { if (@params.lookup_value ("name", null) == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Missing required 'name' parameter. The prompts/get method requires a prompt name to retrieve."); } string name = @params.lookup_value ("name", VariantType.STRING).get_string (); if (name.strip () == "") { throw new Mcp.Core.McpError.INVALID_PARAMS ("The 'name' parameter cannot be empty. Please provide a valid prompt name."); } Variant? arguments = null; if (@params.lookup_value ("arguments", null) != null) { arguments = @params.lookup_value ("arguments", VariantType.VARDICT); } Template? template = templates.lookup (name); if (template == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Prompt '%s' not found. Verify the prompt name is correct and that it has been registered.".printf (name)); } try { var result = yield template.get_prompt (arguments); return result.to_variant (); } catch (Error e) { throw new Mcp.Core.McpError.INTERNAL_ERROR ("Failed to generate prompt '%s': %s".printf (name, e.message)); } } /** * Sends a notification that the prompt list has changed. * * This method is called automatically when templates are registered * or unregistered, but can also be called manually. */ public void notify_list_changed () { list_changed (); } /** * Validates a prompt template before registration. * * @param template The template to validate * @throws Error If validation fails */ private void validate_template (Template template) throws Error { if (template == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Template cannot be null. Please provide a valid Template instance."); } try { var definition = template.get_definition (); if (definition == null) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Template definition cannot be null. The template must return a valid PromptDefinition."); } if (definition.name == null || definition.name.strip () == "") { throw new Mcp.Core.McpError.INVALID_PARAMS ("Template name cannot be empty or null. Please provide a valid name for the prompt template."); } // Validate arguments if (definition.arguments != null) { var argument_names = new Gee.HashSet (); foreach (var arg in definition.arguments) { if (arg.name == null || arg.name.strip () == "") { throw new Mcp.Core.McpError.INVALID_PARAMS ("Argument name cannot be empty or null. All arguments must have valid names."); } if (argument_names.contains (arg.name)) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Duplicate argument name '%s'. Each argument must have a unique name.".printf (arg.name)); } argument_names.add (arg.name); } } } catch (Error e) { throw new Mcp.Core.McpError.INVALID_PARAMS ("Template validation failed: %s. Please check the template implementation and fix any issues.".printf (e.message)); } } } }