fixed 3 tools

This commit is contained in:
2025-07-27 23:31:51 +03:00
parent 1ebc34764b
commit 0a362b37b1
2 changed files with 332 additions and 86 deletions

View File

@@ -26,6 +26,7 @@ import {
Role, Role,
IndexType IndexType
} from "node-appwrite"; } from "node-appwrite";
import { InputFile } from "node-appwrite/file";
import { readFileSync, existsSync } from "fs"; import { readFileSync, existsSync } from "fs";
import { join } from "path"; import { join } from "path";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
@@ -666,12 +667,12 @@ class AppwriteMCPServer {
}, },
{ {
name: "update_user_labels", name: "update_user_labels",
description: "Update user labels", description: "Update user labels (alphanumeric only: A-Z, a-z, 0-9)",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
userId: { type: "string", description: "User ID" }, 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"] required: ["userId", "labels"]
} }
@@ -1664,7 +1665,7 @@ class AppwriteMCPServer {
}, },
{ {
name: "delete_session", name: "delete_session",
description: "Delete a user session", description: "Delete a user session (requires 'account' scope in API key)",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
@@ -2639,12 +2640,12 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}`
break; break;
case 'integer': case 'integer':
attribute = await this.databases.updateIntegerAttribute( attribute = await this.databases.updateIntegerAttribute(
databaseId, collectionId, key, required, min, max, defaultValue databaseId, collectionId, key, required, min, max, defaultValue || null
); );
break; break;
case 'float': case 'float':
attribute = await this.databases.updateFloatAttribute( attribute = await this.databases.updateFloatAttribute(
databaseId, collectionId, key, required, min, max, defaultValue databaseId, collectionId, key, required, min, max, defaultValue || null
); );
break; break;
case 'boolean': case 'boolean':
@@ -3127,13 +3128,26 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}`
const { userId, labels } = args; 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 { return {
content: [ content: [
{ {
type: "text", 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 { userId } = args;
const identities = await this.users.listIdentities(userId) as any; try {
// Try without queries first
const identityList = identities.identities.map((identity: any) => let identities;
`- Provider: ${identity.provider} - ID: ${identity.$id}` try {
).join('\n'); identities = await this.users.listIdentities(userId) as any;
} catch (error: any) {
return { if (error.message.includes('queries')) {
content: [ // If queries error, try with empty array
{ identities = await (this.users as any).listIdentities(userId, []);
type: "text", } else {
text: `User ${userId} identities (${identities.total}):\n${identityList}` 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) { private async deleteUserIdentity(args: any) {
@@ -3525,13 +3561,16 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}`
throw new Error(`File not found: ${filePath}`); throw new Error(`File not found: ${filePath}`);
} }
// For now, return a message indicating file upload capability // Create InputFile from path and upload
// In a full implementation, we would use InputFile.fromPath() 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 { return {
content: [ content: [
{ {
type: "text", 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: [ content: [
{ {
type: "text", 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 { providerId, name, type, enabled } = args;
const pid = providerId || ID.unique(); const pid = providerId || ID.unique();
// Note: Messaging provider creation depends on the specific provider type try {
// For now, return a placeholder response let result;
return {
content: [ // Create provider based on type
{ switch (type) {
type: "text", case 'email':
text: `Messaging provider placeholder created:\n- ID: ${pid}\n- Name: ${name}\n- Type: ${type}\n- Note: Use specific provider methods (createFcmProvider, createMailgunProvider, etc.)` // 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() { private async listMessagingProviders() {
if (!this.messaging) throw new Error("Messaging service not initialized"); if (!this.messaging) throw new Error("Messaging service not initialized");
// Note: Use specific provider list methods (listFcmProviders, listMailgunProviders, etc.) try {
return { const providers = await this.messaging.listProviders() as any;
content: [
{ if (providers.providers.length === 0) {
type: "text", return {
text: `Messaging providers: Use specific provider methods (listFcmProviders, listMailgunProviders, etc.)` 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) { private async getMessagingProvider(args: any) {
@@ -3958,16 +4076,66 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}`
const { providerId, name, enabled } = args; const { providerId, name, enabled } = args;
// Note: Use specific provider update methods (updateFcmProvider, updateMailgunProvider, etc.) try {
// First get the provider to determine its type
const provider = await this.messaging.getProvider(providerId) as any;
return { let result;
content: [ switch (provider.type) {
{ case 'smtp':
type: "text", result = await this.messaging.updateSmtpProvider(
text: `Messaging provider ${providerId} updated successfully` 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) { private async deleteMessagingProvider(args: any) {
@@ -3993,15 +4161,45 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}`
const { messageId, subject, content, topics, users, targets } = args; const { messageId, subject, content, topics, users, targets } = args;
const mid = messageId || ID.unique(); const mid = messageId || ID.unique();
// Note: Use createEmail, createSms, or createPush methods try {
return { // Check if any email providers are available
content: [ const providers = await this.messaging.listProviders() as any;
{ const emailProviders = providers.providers.filter((p: any) =>
type: "text", p.type === 'smtp' || p.type === 'mailgun' || p.type === 'sendgrid'
text: `Message placeholder created:\n- ID: ${mid}\n- Subject: ${subject || 'No subject'}\n- Note: Use specific message methods (createEmail, createSms, createPush)` );
}
] 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) { private async listMessagingMessages(args: any) {
@@ -4245,9 +4443,11 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}`
const countries = await this.locale.listCountries() as any; const countries = await this.locale.listCountries() as any;
const countryList = countries.countries.map((country: any) => const countryList = countries.countries.map((country: any) => {
`- ${country.name} (${country.code}) - Phone: +${country.countryCode}` // Try to find the actual numeric phone code property
).join('\n'); const phoneCode = country.phoneCode || country.dialCode || country.countryCode || country.callingCode || country.dialingCode;
return `- ${country.name} (${country.code}) - Phone: +${phoneCode || 'N/A'}`;
}).join('\n');
return { return {
content: [ content: [
@@ -4319,20 +4519,59 @@ ${attribute.default !== undefined ? `- Default: ${attribute.default}` : ''}`
private async listPhoneCodes() { private async listPhoneCodes() {
if (!this.locale) throw new Error("Locale service not initialized"); 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 countries = await this.locale.listCountries() as any;
const phoneCodeList = countries.countries // Debug: Check actual structure
.filter((country: any) => country.countryCode) if (countries.countries && countries.countries.length > 0) {
.map((country: any) => const sampleCountry = countries.countries[0];
`- ${country.name}: +${country.countryCode}` const availableProps = Object.keys(sampleCountry).join(', ');
).join('\n');
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 { return {
content: [ content: [
{ {
type: "text", 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; const { sessionId } = args;
await this.account.deleteSession(sessionId); try {
await this.account.deleteSession(sessionId);
return { return {
content: [ content: [
{ {
type: "text", type: "text",
text: `Session ${sessionId} deleted successfully` 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) { private async listSessions(args: any) {
@@ -6064,13 +6310,13 @@ For accurate usage tracking, consider:
private async getHealthDb() { private async getHealthDb() {
if (!this.health) throw new Error("Health service not initialized"); 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 { return {
content: [ content: [
{ {
type: "text", 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`
} }
] ]
}; };

View File

@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "ESNext", "module": "Node16",
"moduleResolution": "node", "moduleResolution": "node16",
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,