From 308290c122272dfcd48e29c92ae786cc1d35da9b Mon Sep 17 00:00:00 2001 From: smoido Date: Wed, 29 Oct 2025 01:14:07 +0300 Subject: [PATCH] fix: query parsing, attribute size updates, and detailed attribute display --- src/index.ts | 148 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9a0afce..572bfff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -735,8 +735,22 @@ class AppwriteMCPServer { case "get": if (!key) throw new Error("key is required for get action"); const attribute = await this.databases.getAttribute(databaseId, collectionId, key); + const attr = attribute as any; + + // Build detailed attribute information + let details = `Attribute Details:\n- Key: ${attr.key}\n- Type: ${attr.type}\n- Required: ${attr.required}\n- Status: ${attr.status}`; + + // Add type-specific details + if (attr.size !== undefined) details += `\n- Size: ${attr.size}`; + if (attr.min !== undefined) details += `\n- Min: ${attr.min}`; + if (attr.max !== undefined) details += `\n- Max: ${attr.max}`; + if (attr.default !== undefined && attr.default !== null) details += `\n- Default: ${attr.default}`; + if (attr.array !== undefined) details += `\n- Array: ${attr.array}`; + if (attr.elements !== undefined) details += `\n- Elements: ${attr.elements.join(', ')}`; + if (attr.format !== undefined) details += `\n- Format: ${attr.format}`; + return { - content: [{ type: "text", text: `Attribute Details:\n- Key: ${(attribute as any).key}\n- Type: ${(attribute as any).type}\n- Required: ${(attribute as any).required}\n- Status: ${(attribute as any).status}` }] + content: [{ type: "text", text: details }] }; case "list": @@ -753,7 +767,16 @@ class AppwriteMCPServer { let updateResult; switch (type) { case "string": - updateResult = await this.databases.updateStringAttribute(databaseId, collectionId, key, required, otherArgs.default); + // Appwrite API requires default param, use null for required attrs if not provided + const strDefault = otherArgs.default !== undefined ? otherArgs.default : null; + updateResult = await this.databases.updateStringAttribute( + databaseId, + collectionId, + key, + required, + strDefault as any, + otherArgs.size + ); break; case "integer": updateResult = await this.databases.updateIntegerAttribute(databaseId, collectionId, key, required, otherArgs.min, otherArgs.max, otherArgs.default); @@ -971,12 +994,127 @@ class AppwriteMCPServer { } } + private parseQuery(queryString: string): string { + // Parse query strings like 'equal("field", "value")' into proper Query objects + // This regex matches: functionName("field", value) or functionName("field", [values]) + const match = queryString.match(/^(\w+)\((.*)\)$/); + if (!match) { + throw new Error(`Invalid query format: ${queryString}. Expected format: equal("field", "value")`); + } + + const [, method, argsStr] = match; + + // Parse arguments - handle quoted strings and arrays + const args: any[] = []; + let current = ''; + let inQuotes = false; + let inArray = false; + + for (let i = 0; i < argsStr.length; i++) { + const char = argsStr[i]; + + if (char === '"' && argsStr[i - 1] !== '\\') { + inQuotes = !inQuotes; + continue; + } + + if (char === '[' && !inQuotes) { + inArray = true; + current += char; + continue; + } + + if (char === ']' && !inQuotes) { + inArray = false; + current += char; + continue; + } + + if (char === ',' && !inQuotes && !inArray) { + args.push(current.trim()); + current = ''; + continue; + } + + current += char; + } + + if (current.trim()) { + args.push(current.trim()); + } + + // Convert parsed arguments to proper types + const processedArgs = args.map(arg => { + // Handle arrays + if (arg.startsWith('[') && arg.endsWith(']')) { + const items = arg.slice(1, -1).split(',').map((item: string) => item.trim().replace(/^"(.*)"$/, '$1')); + return items; + } + // Handle numbers + if (/^-?\d+(\.\d+)?$/.test(arg)) { + return Number(arg); + } + // Handle booleans + if (arg === 'true') return true; + if (arg === 'false') return false; + // Handle strings (remove quotes) + return arg.replace(/^"(.*)"$/, '$1'); + }); + + // Map to Query methods + const queryMap: Record string> = { + equal: Query.equal, + notEqual: Query.notEqual, + lessThan: Query.lessThan, + lessThanEqual: Query.lessThanEqual, + greaterThan: Query.greaterThan, + greaterThanEqual: Query.greaterThanEqual, + search: Query.search, + isNull: Query.isNull, + isNotNull: Query.isNotNull, + between: Query.between, + startsWith: Query.startsWith, + endsWith: Query.endsWith, + select: Query.select, + orderDesc: Query.orderDesc, + orderAsc: Query.orderAsc, + limit: Query.limit, + offset: Query.offset, + contains: Query.contains, + or: Query.or, + and: Query.and, + }; + + if (!queryMap[method]) { + throw new Error(`Unknown query method: ${method}. Available methods: ${Object.keys(queryMap).join(', ')}`); + } + + return queryMap[method](...processedArgs); + } + private async listDocuments(args: any) { if (!this.databases) throw new Error("Databases not initialized"); - + const { databaseId, collectionId, queries, limit, offset } = args; - const documents = await this.databases.listDocuments(databaseId, collectionId, queries || []); - + + // Parse query strings into Query objects + const parsedQueries: string[] = []; + if (queries && Array.isArray(queries)) { + for (const queryStr of queries) { + try { + parsedQueries.push(this.parseQuery(queryStr)); + } catch (error) { + throw new Error(`Failed to parse query "${queryStr}": ${error instanceof Error ? error.message : String(error)}`); + } + } + } + + // Add limit and offset if provided + if (limit) parsedQueries.push(Query.limit(limit)); + if (offset) parsedQueries.push(Query.offset(offset)); + + const documents = await this.databases.listDocuments(databaseId, collectionId, parsedQueries); + const docList = documents.documents.map(doc => `- ${doc.$id} (updated: ${doc.$updatedAt})`).join('\n'); return { content: [{ type: "text", text: `Documents in collection ${collectionId} (${documents.total}):\n${docList}` }]