From 0a362b37b184e8113db237c35ffb4242d8c02650 Mon Sep 17 00:00:00 2001 From: smoido Date: Sun, 27 Jul 2025 23:31:51 +0300 Subject: [PATCH] fixed 3 tools --- src/index.ts | 414 ++++++++++++++++++++++++++++++++++++++++---------- tsconfig.json | 4 +- 2 files changed, 332 insertions(+), 86 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5a25341..52f3cd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import { Role, IndexType } from "node-appwrite"; +import { InputFile } from "node-appwrite/file"; import { readFileSync, existsSync } from "fs"; import { join } from "path"; import * as dotenv from "dotenv"; @@ -666,12 +667,12 @@ class AppwriteMCPServer { }, { name: "update_user_labels", - description: "Update user labels", + description: "Update user labels (alphanumeric only: A-Z, a-z, 0-9)", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User ID" }, - labels: { type: "array", description: "User labels", items: { type: "string" } } + labels: { type: "array", description: "User labels (max 1000, 1-36 chars each, alphanumeric only)", items: { type: "string" } } }, required: ["userId", "labels"] } @@ -1664,7 +1665,7 @@ class AppwriteMCPServer { }, { name: "delete_session", - description: "Delete a user session", + description: "Delete a user session (requires 'account' scope in API key)", inputSchema: { type: "object", properties: { @@ -2639,12 +2640,12 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` break; case 'integer': attribute = await this.databases.updateIntegerAttribute( - databaseId, collectionId, key, required, min, max, defaultValue + databaseId, collectionId, key, required, min, max, defaultValue || null ); break; case 'float': attribute = await this.databases.updateFloatAttribute( - databaseId, collectionId, key, required, min, max, defaultValue + databaseId, collectionId, key, required, min, max, defaultValue || null ); break; case 'boolean': @@ -3127,13 +3128,26 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` const { userId, labels } = args; - const user = await this.users.updateLabels(userId, labels) as any; + // Validate labels: alphanumeric only, 1-36 chars each + const validLabels = labels.filter((label: string) => { + const isValid = /^[A-Za-z0-9]{1,36}$/.test(label); + if (!isValid) { + console.warn(`Invalid label: "${label}" - must be alphanumeric, 1-36 characters`); + } + return isValid; + }); + + if (validLabels.length !== labels.length) { + throw new Error(`Invalid labels detected. Labels must be alphanumeric (A-Z, a-z, 0-9) and 1-36 characters long. Invalid: ${labels.filter((l: string) => !/^[A-Za-z0-9]{1,36}$/.test(l)).join(', ')}`); + } + + const user = await this.users.updateLabels(userId, validLabels) as any; return { content: [ { type: "text", - text: `User ${userId} labels updated:\n${labels.join(', ')}` + text: `User ${userId} labels updated:\n${validLabels.join(', ')}` } ] }; @@ -3178,20 +3192,42 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` const { userId } = args; - const identities = await this.users.listIdentities(userId) as any; - - const identityList = identities.identities.map((identity: any) => - `- Provider: ${identity.provider} - ID: ${identity.$id}` - ).join('\n'); - - return { - content: [ - { - type: "text", - text: `User ${userId} identities (${identities.total}):\n${identityList}` + try { + // Try without queries first + let identities; + try { + identities = await this.users.listIdentities(userId) as any; + } catch (error: any) { + if (error.message.includes('queries')) { + // If queries error, try with empty array + identities = await (this.users as any).listIdentities(userId, []); + } else { + throw error; } - ] - }; + } + + const identityList = identities.identities.map((identity: any) => + `- Provider: ${identity.provider} - ID: ${identity.$id}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `User ${userId} identities (${identities.total}):\n${identityList}` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error listing identities: ${error.message}` + } + ] + }; + } } private async deleteUserIdentity(args: any) { @@ -3525,13 +3561,16 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` throw new Error(`File not found: ${filePath}`); } - // For now, return a message indicating file upload capability - // In a full implementation, we would use InputFile.fromPath() + // Create InputFile from path and upload + const fileName = filePath.split(/[\\/]/).pop() || 'file'; + const file = InputFile.fromPath(filePath, fileName); + const result = await this.storage.createFile(bucketId, fid, file, permissions) as any; + return { content: [ { type: "text", - text: `File upload prepared for bucket ${bucketId}:\n- Target ID: ${fid}\n- Source: ${filePath}\n- Note: File upload requires InputFile implementation` + text: `File uploaded successfully:\n- File ID: ${result.$id}\n- Name: ${result.name}\n- Size: ${result.sizeOriginal} bytes\n- MIME Type: ${result.mimeType}` } ] }; @@ -3540,7 +3579,7 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` content: [ { type: "text", - text: `Error preparing file upload: ${error.message}` + text: `Error uploading file: ${error.message}` } ] }; @@ -3910,30 +3949,109 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` const { providerId, name, type, enabled } = args; const pid = providerId || ID.unique(); - // Note: Messaging provider creation depends on the specific provider type - // For now, return a placeholder response - return { - content: [ - { - type: "text", - text: `Messaging provider placeholder created:\n- ID: ${pid}\n- Name: ${name}\n- Type: ${type}\n- Note: Use specific provider methods (createFcmProvider, createMailgunProvider, etc.)` - } - ] - }; + try { + let result; + + // Create provider based on type + switch (type) { + case 'email': + // Create SMTP provider (most common email provider) + result = await this.messaging.createSmtpProvider( + pid, + name, + 'smtp.gmail.com', // default host + 587, // default port + '', // username (to be configured later) + '', // password (to be configured later) + 'tls' as any, // encryption + false, // autoTLS + '' as any, // mailer + enabled || true + ) as any; + break; + case 'sms': + // Create Twilio provider (most common SMS provider) + result = await this.messaging.createTwilioProvider( + pid, + name, + '', // Account SID (to be configured later) + '', // Auth token (to be configured later) + '', // From number (to be configured later) + enabled || true + ) as any; + break; + case 'push': + // Create FCM provider (most common push provider) + result = await this.messaging.createFcmProvider( + pid, + name, + {}, // Service account JSON (to be configured later) + enabled || true + ) as any; + break; + default: + throw new Error(`Unsupported provider type: ${type}. Use 'email', 'sms', or 'push'`); + } + + return { + content: [ + { + type: "text", + text: `${type.toUpperCase()} provider created:\n- ID: ${result.$id}\n- Name: ${result.name}\n- Type: ${result.type}\n- Enabled: ${result.enabled}\n- Note: Configure credentials in Appwrite Console` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error creating ${type} provider: ${error.message}` + } + ] + }; + } } private async listMessagingProviders() { if (!this.messaging) throw new Error("Messaging service not initialized"); - // Note: Use specific provider list methods (listFcmProviders, listMailgunProviders, etc.) - return { - content: [ - { - type: "text", - text: `Messaging providers: Use specific provider methods (listFcmProviders, listMailgunProviders, etc.)` - } - ] - }; + try { + const providers = await this.messaging.listProviders() as any; + + if (providers.providers.length === 0) { + return { + content: [ + { + type: "text", + text: "No messaging providers configured" + } + ] + }; + } + + const providerList = providers.providers.map((provider: any) => + `- ${provider.name} (${provider.type}) - ID: ${provider.$id} - Enabled: ${provider.enabled}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Messaging providers (${providers.providers.length}):\n${providerList}` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error listing providers: ${error.message}` + } + ] + }; + } } private async getMessagingProvider(args: any) { @@ -3958,16 +4076,66 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` const { providerId, name, enabled } = args; - // Note: Use specific provider update methods (updateFcmProvider, updateMailgunProvider, etc.) - - return { - content: [ - { - type: "text", - text: `Messaging provider ${providerId} updated successfully` - } - ] - }; + try { + // First get the provider to determine its type + const provider = await this.messaging.getProvider(providerId) as any; + + let result; + switch (provider.type) { + case 'smtp': + result = await this.messaging.updateSmtpProvider( + providerId, + name || provider.name, + provider.host, + provider.port, + provider.username, + provider.password, + provider.encryption, + provider.autoTLS, + provider.mailer, + enabled !== undefined ? enabled : provider.enabled + ) as any; + break; + case 'twilio': + result = await this.messaging.updateTwilioProvider( + providerId, + name || provider.name, + provider.accountSid, + provider.authToken, + provider.from, + enabled !== undefined ? enabled : provider.enabled + ) as any; + break; + case 'fcm': + result = await this.messaging.updateFcmProvider( + providerId, + name || provider.name, + provider.serviceAccountJSON, + enabled !== undefined ? enabled : provider.enabled + ) as any; + break; + default: + throw new Error(`Unsupported provider type: ${provider.type}`); + } + + return { + content: [ + { + type: "text", + text: `${provider.type.toUpperCase()} provider updated:\n- ID: ${result.$id}\n- Name: ${result.name}\n- Enabled: ${result.enabled}` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error updating provider: ${error.message}` + } + ] + }; + } } private async deleteMessagingProvider(args: any) { @@ -3993,15 +4161,45 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` const { messageId, subject, content, topics, users, targets } = args; const mid = messageId || ID.unique(); - // Note: Use createEmail, createSms, or createPush methods - return { - content: [ - { - type: "text", - text: `Message placeholder created:\n- ID: ${mid}\n- Subject: ${subject || 'No subject'}\n- Note: Use specific message methods (createEmail, createSms, createPush)` - } - ] - }; + try { + // Check if any email providers are available + const providers = await this.messaging.listProviders() as any; + const emailProviders = providers.providers.filter((p: any) => + p.type === 'smtp' || p.type === 'mailgun' || p.type === 'sendgrid' + ); + + if (emailProviders.length === 0) { + throw new Error("No email providers configured. Please create an email provider first using create_messaging_provider."); + } + + // Create email message (assuming email provider since most common) + const result = await this.messaging.createEmail( + mid, + subject || "No Subject", + content, + topics, + users, + targets + ) as any; + + return { + content: [ + { + type: "text", + text: `Email message created and queued:\n- ID: ${result.$id}\n- Subject: ${result.subject}\n- Status: ${result.status}\n- Scheduled: ${result.scheduledAt || 'Immediate'}` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error creating message: ${error.message}` + } + ] + }; + } } private async listMessagingMessages(args: any) { @@ -4245,9 +4443,11 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` const countries = await this.locale.listCountries() as any; - const countryList = countries.countries.map((country: any) => - `- ${country.name} (${country.code}) - Phone: +${country.countryCode}` - ).join('\n'); + const countryList = countries.countries.map((country: any) => { + // Try to find the actual numeric phone code property + const phoneCode = country.phoneCode || country.dialCode || country.countryCode || country.callingCode || country.dialingCode; + return `- ${country.name} (${country.code}) - Phone: +${phoneCode || 'N/A'}`; + }).join('\n'); return { content: [ @@ -4319,20 +4519,59 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}` private async listPhoneCodes() { if (!this.locale) throw new Error("Locale service not initialized"); - // Use listCountries which includes phone codes + try { + // Try using the dedicated listPhones API if available + const phones = await (this.locale as any).listPhones() as any; + + if (phones && phones.phones) { + const phoneCodeList = phones.phones.map((phone: any) => + `- ${phone.countryName}: +${phone.countryCode}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Phone codes (${phones.phones.length}):\n${phoneCodeList}` + } + ] + }; + } + } catch (error) { + // Fall back to countries endpoint + } + + // Fallback: Use listCountries const countries = await this.locale.listCountries() as any; - const phoneCodeList = countries.countries - .filter((country: any) => country.countryCode) - .map((country: any) => - `- ${country.name}: +${country.countryCode}` - ).join('\n'); + // Debug: Check actual structure + if (countries.countries && countries.countries.length > 0) { + const sampleCountry = countries.countries[0]; + const availableProps = Object.keys(sampleCountry).join(', '); + + const phoneCodeList = countries.countries + .map((country: any) => { + const phoneCode = country.phoneCode || country.dialCode || country.countryCode || country.callingCode; + return phoneCode ? `- ${country.name}: +${phoneCode}` : null; + }) + .filter(Boolean) + .join('\n'); + + return { + content: [ + { + type: "text", + text: `Phone codes (${countries.countries.length}):\n${phoneCodeList || 'No phone codes found'}\n\nDebug - Sample country properties: ${availableProps}` + } + ] + }; + } return { content: [ { type: "text", - text: `Phone codes (${countries.countries.length}):\n${phoneCodeList}` + text: `No countries data found. Response structure: ${JSON.stringify(countries, null, 2)}` } ] }; @@ -5956,16 +6195,23 @@ For accurate usage tracking, consider: const { sessionId } = args; - await this.account.deleteSession(sessionId); - - return { - content: [ - { - type: "text", - text: `Session ${sessionId} deleted successfully` - } - ] - }; + try { + await this.account.deleteSession(sessionId); + + return { + content: [ + { + type: "text", + text: `Session ${sessionId} deleted successfully` + } + ] + }; + } catch (error: any) { + if (error.message.includes('missing scope (account)')) { + throw new Error("API key missing 'account' scope. Please add 'account.read' and 'account.write' permissions to your API key in the Appwrite Console."); + } + throw error; + } } private async listSessions(args: any) { @@ -6064,13 +6310,13 @@ For accurate usage tracking, consider: private async getHealthDb() { if (!this.health) throw new Error("Health service not initialized"); - const health = await this.health.getDB(); + const health = await this.health.getDB() as any; return { content: [ { type: "text", - text: `Database Health: ${health.status}\n- Ping: ${health.ping}ms` + text: `Database Health: ${health.status || 'OK'}\n- Ping: ${health.ping || health.duration || 'N/A'}ms` } ] }; diff --git a/tsconfig.json b/tsconfig.json index d51cffc..0bcb083 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "ESNext", - "moduleResolution": "node", + "module": "Node16", + "moduleResolution": "node16", "outDir": "./dist", "rootDir": "./src", "strict": true,