|
@@ -0,0 +1,161 @@
|
|
|
|
|
+import { ShopDatabase } from '../../database/Database';
|
|
|
|
|
+import { logger } from '../../utils/logger';
|
|
|
|
|
+
|
|
|
|
|
+export interface ToolResult {
|
|
|
|
|
+ content: Array<{
|
|
|
|
|
+ type: 'text';
|
|
|
|
|
+ text: string;
|
|
|
|
|
+ }>;
|
|
|
|
|
+ isError?: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Map content types to their search tools
|
|
|
|
|
+const CONTENT_TYPE_TO_TOOL: Record<string, string> = {
|
|
|
|
|
+ shipping: 'shipping_search',
|
|
|
|
|
+ contacts: 'contacts_search',
|
|
|
|
|
+ terms: 'terms_search',
|
|
|
|
|
+ faq: 'faq_search',
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export class ListContentsTool {
|
|
|
|
|
+ constructor(private database: ShopDatabase) {}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Execute the tool - list all content titles with their associated search tools
|
|
|
|
|
+ */
|
|
|
|
|
+ async execute(args: any): Promise<ToolResult> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Validate arguments
|
|
|
|
|
+ const validation = this.validateArgs(args);
|
|
|
|
|
+ if (!validation.valid) {
|
|
|
|
|
+ return this.createErrorResult(validation.error || 'Invalid arguments');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const shopId = String(args.shop_id);
|
|
|
|
|
+
|
|
|
|
|
+ // Check if shop exists
|
|
|
|
|
+ const shop = this.database.getShopByCustomId(shopId);
|
|
|
|
|
+ if (!shop) {
|
|
|
|
|
+ return this.createErrorResult('Shop not found');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Get all content for this shop (only enabled content)
|
|
|
|
|
+ const allContent = this.database.getAllShopContent(shop.id, false);
|
|
|
|
|
+
|
|
|
|
|
+ if (allContent.length === 0) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: `No content available for shop "${shopId}". The shop may not have been scraped yet.`,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Group content by type and get unique titles
|
|
|
|
|
+ const contentByType: Record<string, string[]> = {
|
|
|
|
|
+ shipping: [],
|
|
|
|
|
+ contacts: [],
|
|
|
|
|
+ terms: [],
|
|
|
|
|
+ faq: [],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Use a Set to track unique URLs per type (to avoid duplicates from versioning)
|
|
|
|
|
+ const seenUrls: Record<string, Set<string>> = {
|
|
|
|
|
+ shipping: new Set(),
|
|
|
|
|
+ contacts: new Set(),
|
|
|
|
|
+ terms: new Set(),
|
|
|
|
|
+ faq: new Set(),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ for (const content of allContent) {
|
|
|
|
|
+ const type = content.content_type;
|
|
|
|
|
+ if (contentByType[type] && !seenUrls[type].has(content.url)) {
|
|
|
|
|
+ seenUrls[type].add(content.url);
|
|
|
|
|
+ contentByType[type].push(content.title || 'Untitled');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Format the response - simple list for voice/phone use
|
|
|
|
|
+ const lines: string[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (const [type, titles] of Object.entries(contentByType)) {
|
|
|
|
|
+ if (titles.length > 0) {
|
|
|
|
|
+ const toolName = CONTENT_TYPE_TO_TOOL[type];
|
|
|
|
|
+ for (const title of titles) {
|
|
|
|
|
+ lines.push(`${title} - ${toolName}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const responseText = lines.join('\n');
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: responseText,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ logger.error('Error in list_contents tool:', error);
|
|
|
|
|
+ return this.createErrorResult(error.message || 'Failed to list contents');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Validate tool arguments
|
|
|
|
|
+ */
|
|
|
|
|
+ private validateArgs(args: any): { valid: boolean; error?: string } {
|
|
|
|
|
+ if (!args || typeof args !== 'object') {
|
|
|
|
|
+ return { valid: false, error: 'Arguments must be an object' };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!args.shop_id || typeof args.shop_id !== 'string') {
|
|
|
|
|
+ return { valid: false, error: 'Missing required parameter: shop_id' };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { valid: true };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Create error result
|
|
|
|
|
+ */
|
|
|
|
|
+ private createErrorResult(message: string): ToolResult {
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: message,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ isError: true,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Get tool description for MCP
|
|
|
|
|
+ */
|
|
|
|
|
+ getDescription(): string {
|
|
|
|
|
+ return 'List all available content titles for a webshop with their associated search tools. Use this tool first to discover what content is available and which search tool to use for finding specific information.';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Get input schema for MCP
|
|
|
|
|
+ */
|
|
|
|
|
+ getInputSchema(): object {
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ shop_id: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ description: 'The unique identifier of the webshop to list contents for',
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ required: ['shop_id'],
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|