From d0db180121374ecacd1ac5c8048da896d9830488 Mon Sep 17 00:00:00 2001 From: Midoshki Date: Thu, 24 Jul 2025 19:21:55 +0300 Subject: [PATCH] Initial commit: Complete Appwrite MCP Server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Comprehensive Appwrite integration with 50+ tools - Smart schema operations (auto-detect, validate, migrate) - Data analysis and insights capabilities - Full CRUD operations for databases, collections, documents - User management, storage, functions, and teams support - Removed problematic natural language query features - Clean TypeScript implementation with proper error handling ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 50 + README.md | 249 +++ appwrite-config.json.template | 5 + package-lock.json | 1101 ++++++++++++ package.json | 23 + src/index.ts | 3173 +++++++++++++++++++++++++++++++++ tsconfig.json | 19 + 7 files changed, 4620 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 appwrite-config.json.template create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56d9c34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ + +# Environment and config files +appwrite-config.json +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +thumbs.db + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# Temporary folders +tmp/ +temp/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..014a4ca --- /dev/null +++ b/README.md @@ -0,0 +1,249 @@ +# Appwrite MCP Server + +A comprehensive Model Context Protocol (MCP) server that transforms Claude Code into an intelligent Appwrite management assistant. + +## ๐Ÿš€ Features + +**Core Appwrite Management:** +- Complete database and collection operations +- Document CRUD with full query support +- User management and authentication +- Storage buckets and file operations +- Serverless function management +- Team and permission management + +**๐Ÿง  Intelligent Database Assistant:** +- **Auto-detect schemas** from sample data +- **Data quality analysis** and recommendations +- **Duplicate detection** with similarity matching +- **Index optimization** suggestions +- **Schema migrations** with safety checks + +## Installation + +1. Clone this repository: +```bash +git clone +cd appwrite-mcp-server +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Build the TypeScript code: +```bash +npm run build +``` + +4. Configure your Appwrite credentials: +```bash +cp appwrite-config.json.template appwrite-config.json +``` + +5. Edit `appwrite-config.json` with your Appwrite project details: +```json +{ + "projectId": "your-project-id", + "apiEndpoint": "https://cloud.appwrite.io/v1", + "apiKey": "your-api-key" +} +``` + +## Adding to Claude Code + +1. Add the MCP server to Claude Code: +```bash +claude mcp add appwrite-mcp-server +``` + +2. When prompted, provide the absolute path to the built server: +``` +/absolute/path/to/your/app-write-mcp/dist/index.js +``` + +3. Restart Claude Code if needed. + +## Available Tools + +### ๐Ÿ—„๏ธ Database Operations + +- **create_database**: Create a new database +- **list_databases**: List all databases in your project +- **delete_database**: Delete a database + +### ๐Ÿ“ Collection Operations + +- **create_collection**: Create a new collection in a database +- **list_collections**: List all collections in a database +- **delete_collection**: Delete a collection from a database + +### ๐Ÿท๏ธ Attribute Management + +- **create_string_attribute**: Create a string attribute in a collection +- **create_integer_attribute**: Create an integer attribute in a collection +- **create_boolean_attribute**: Create a boolean attribute in a collection +- **create_email_attribute**: Create an email attribute in a collection +- **create_datetime_attribute**: Create a datetime attribute in a collection +- **list_attributes**: List all attributes in a collection +- **delete_attribute**: Delete an attribute from a collection + +### ๐Ÿ“Š Index Management + +- **create_index**: Create an index in a collection (key, fulltext, unique) +- **list_indexes**: List all indexes in a collection +- **delete_index**: Delete an index from a collection + +### ๐Ÿ“„ Document Operations + +- **create_document**: Create a new document in a collection +- **get_document**: Get a document by ID +- **list_documents**: List documents in a collection with optional queries +- **update_document**: Update a document +- **delete_document**: Delete a document + +### ๐Ÿ‘ฅ User Management + +- **create_user**: Create a new user +- **list_users**: List all users +- **get_user**: Get a user by ID +- **update_user_email**: Update user email +- **update_user_name**: Update user name +- **update_user_password**: Update user password +- **delete_user**: Delete a user + +### ๐Ÿ—‚๏ธ Storage Operations + +- **create_bucket**: Create a storage bucket +- **list_buckets**: List all storage buckets +- **get_bucket**: Get a bucket by ID +- **update_bucket**: Update a storage bucket +- **delete_bucket**: Delete a storage bucket +- **list_files**: List files in a storage bucket + +### โšก Function Management + +- **create_function**: Create a new serverless function +- **list_functions**: List all functions +- **get_function**: Get a function by ID +- **update_function**: Update a function +- **delete_function**: Delete a function + +### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Team Management + +- **create_team**: Create a new team +- **list_teams**: List all teams +- **get_team**: Get a team by ID +- **update_team**: Update a team +- **delete_team**: Delete a team + +### ๐Ÿง  Smart Schema Operations + +- **auto_detect_schema**: Analyze sample data and automatically create collection schema +- **suggest_indexes**: Recommend optimal indexes based on collection usage patterns +- **validate_document**: Check document data against collection schema before creation +- **schema_migration**: Automatically migrate collection schema with data preservation + + +### ๐Ÿ“Š Data Analysis & Insights + +- **analyze_collection**: Get comprehensive data insights and patterns from collections +- **detect_duplicates**: Find potential duplicate records with similarity scoring +- **data_quality_check**: Analyze data quality (completeness, validity, consistency) +- **usage_stats**: Get usage statistics and access patterns for collections + +## Configuration + +### Getting Appwrite Credentials + +1. **Project ID**: Found in your Appwrite console under Settings > General +2. **API Endpoint**: + - For Appwrite Cloud: `https://cloud.appwrite.io/v1` + - For self-hosted: `https://your-domain.com/v1` +3. **API Key**: Create a new API key in your Appwrite console under Settings > API Keys + - Make sure to give it appropriate permissions for databases and collections + +### Configuration File + +The `appwrite-config.json` file should be in the root directory of this project: + +```json +{ + "projectId": "your-project-id-here", + "apiEndpoint": "https://cloud.appwrite.io/v1", + "apiKey": "your-api-key-here" +} +``` + +## Usage Examples + +Once added to Claude Code, you can use natural language to interact with Appwrite: + +**Database & Collection Management:** +- "Create a new database called 'blog'" +- "List all my databases" +- "Create a collection called 'posts' in the blog database" +- "Show me all collections in the blog database" + +**Attribute & Schema Management:** +- "Add a string attribute called 'title' to the posts collection with max size 255" +- "Create a boolean attribute 'published' in the posts collection" +- "List all attributes in the posts collection" +- "Create a unique index on the 'slug' attribute" + +**Document Operations:** +- "Create a new document in the posts collection with title 'Hello World'" +- "List all documents in the posts collection" +- "Update document xyz123 in the posts collection" +- "Get document abc456 from the posts collection" + +**User Management:** +- "Create a new user with email john@example.com" +- "List all users in the project" +- "Update user xyz123's name to 'John Doe'" +- "Delete user abc456" + +**Storage Management:** +- "Create a storage bucket called 'images'" +- "List all storage buckets" +- "Show me all files in the images bucket" + +**Function Management:** +- "Create a new function called 'sendEmail' with Node.js runtime" +- "List all functions" +- "Update function abc123's timeout to 300 seconds" + +**Team Management:** +- "Create a new team called 'Developers'" +- "List all teams" +- "Add user john@example.com to the Developers team" + +**๐Ÿง  Smart Schema Operations:** +- "Analyze this sample data and create a collection for me" +- "Suggest optimal indexes for my users collection" +- "Validate this document before I create it" +- "Migrate my collection to add these new fields safely" + + +**๐Ÿ“Š Data Analysis & Insights:** +- "Analyze the health and patterns in my users collection" +- "Find duplicate records in my products collection" +- "Check the data quality of my orders collection" +- "Show me usage statistics for my database" + +## Development + +- `npm run build`: Build TypeScript to JavaScript +- `npm run dev`: Watch mode for development +- `npm start`: Run the built server + +## Troubleshooting + +1. **Configuration not found**: Make sure `appwrite-config.json` exists in the project root +2. **API Key issues**: Ensure your API key has the correct permissions for database operations +3. **Connection issues**: Verify your API endpoint is correct (especially for self-hosted instances) + +## License + +MIT \ No newline at end of file diff --git a/appwrite-config.json.template b/appwrite-config.json.template new file mode 100644 index 0000000..5607edf --- /dev/null +++ b/appwrite-config.json.template @@ -0,0 +1,5 @@ +{ + "projectId": "YOUR_PROJECT_ID_HERE", + "apiEndpoint": "YOUR_API_ENDPOINT_HERE", + "apiKey": "YOUR_API_KEY_HERE" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..61dfb79 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1101 @@ +{ + "name": "appwrite-mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "appwrite-mcp-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "node-appwrite": "^14.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.16.0.tgz", + "integrity": "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-appwrite": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-14.2.0.tgz", + "integrity": "sha512-sPPA+JzdBJRS+lM6azX85y3/6iyKQYlHcXCbjMuWLROh6IiU9EfXRW3XSUTa5HDoBrlo8ve+AnVA6BIjQfUs1g==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-native-with-agent": "1.7.2" + } + }, + "node_modules/node-fetch-native-with-agent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz", + "integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bc6ffe7 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "appwrite-mcp-server", + "version": "1.0.0", + "description": "Model Context Protocol server for Appwrite integration", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsc --watch" + }, + "keywords": ["mcp", "appwrite", "claude", "model-context-protocol"], + "author": "", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "node-appwrite": "^14.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1c42b53 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3173 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ErrorCode, + ListToolsRequestSchema, + McpError, +} from "@modelcontextprotocol/sdk/types.js"; +import { + Client, + Databases, + Users, + Storage, + Functions, + Teams, + ID, + Query, + Permission, + Role, + IndexType +} from "node-appwrite"; +import { readFileSync, existsSync } from "fs"; +import { join } from "path"; + +interface AppwriteConfig { + projectId: string; + apiEndpoint: string; + apiKey: string; +} + +class AppwriteMCPServer { + private server: Server; + private client: Client | null = null; + private databases: Databases | null = null; + private users: Users | null = null; + private storage: Storage | null = null; + private functions: Functions | null = null; + private teams: Teams | null = null; + private config: AppwriteConfig | null = null; + + constructor() { + this.server = new Server( + { + name: "appwrite-mcp-server", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + } + ); + + this.setupToolHandlers(); + this.loadConfig(); + } + + private loadConfig(): void { + const configPath = join(process.cwd(), "appwrite-config.json"); + + if (!existsSync(configPath)) { + console.error("Appwrite configuration file not found. Please create appwrite-config.json"); + return; + } + + try { + const configData = readFileSync(configPath, "utf-8"); + this.config = JSON.parse(configData); + + if (!this.config?.projectId || !this.config?.apiEndpoint || !this.config?.apiKey) { + throw new Error("Missing required configuration fields"); + } + + this.initializeAppwrite(); + } catch (error) { + console.error("Error loading Appwrite configuration:", error); + } + } + + private initializeAppwrite(): void { + if (!this.config) return; + + this.client = new Client() + .setEndpoint(this.config.apiEndpoint) + .setProject(this.config.projectId) + .setKey(this.config.apiKey); + + this.databases = new Databases(this.client); + this.users = new Users(this.client); + this.storage = new Storage(this.client); + this.functions = new Functions(this.client); + this.teams = new Teams(this.client); + } + + private setupToolHandlers(): void { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + // Database Operations + { + name: "create_database", + description: "Create a new database in Appwrite", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "Unique ID for the database (optional)" }, + name: { type: "string", description: "Name of the database" } + }, + required: ["name"] + } + }, + { + name: "list_databases", + description: "List all databases in the Appwrite project", + inputSchema: { type: "object", properties: {} } + }, + { + name: "delete_database", + description: "Delete a database from Appwrite", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database to delete" } + }, + required: ["databaseId"] + } + }, + + // Collection Operations + { + name: "create_collection", + description: "Create a new collection in a database", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "Unique ID for the collection (optional)" }, + name: { type: "string", description: "Name of the collection" }, + permissions: { type: "array", description: "Collection permissions (optional)", items: { type: "string" } } + }, + required: ["databaseId", "name"] + } + }, + { + name: "list_collections", + description: "List all collections in a database", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" } + }, + required: ["databaseId"] + } + }, + { + name: "delete_collection", + description: "Delete a collection from a database", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection to delete" } + }, + required: ["databaseId", "collectionId"] + } + }, + + // Attribute Operations + { + name: "create_string_attribute", + description: "Create a string attribute in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + key: { type: "string", description: "Attribute key" }, + size: { type: "number", description: "Maximum size of the string" }, + required: { type: "boolean", description: "Is attribute required" }, + default: { type: "string", description: "Default value (optional)" } + }, + required: ["databaseId", "collectionId", "key", "size", "required"] + } + }, + { + name: "create_integer_attribute", + description: "Create an integer attribute in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + key: { type: "string", description: "Attribute key" }, + required: { type: "boolean", description: "Is attribute required" }, + min: { type: "number", description: "Minimum value (optional)" }, + max: { type: "number", description: "Maximum value (optional)" }, + default: { type: "number", description: "Default value (optional)" } + }, + required: ["databaseId", "collectionId", "key", "required"] + } + }, + { + name: "create_boolean_attribute", + description: "Create a boolean attribute in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + key: { type: "string", description: "Attribute key" }, + required: { type: "boolean", description: "Is attribute required" }, + default: { type: "boolean", description: "Default value (optional)" } + }, + required: ["databaseId", "collectionId", "key", "required"] + } + }, + { + name: "create_email_attribute", + description: "Create an email attribute in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + key: { type: "string", description: "Attribute key" }, + required: { type: "boolean", description: "Is attribute required" }, + default: { type: "string", description: "Default value (optional)" } + }, + required: ["databaseId", "collectionId", "key", "required"] + } + }, + { + name: "create_datetime_attribute", + description: "Create a datetime attribute in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + key: { type: "string", description: "Attribute key" }, + required: { type: "boolean", description: "Is attribute required" }, + default: { type: "string", description: "Default value (optional)" } + }, + required: ["databaseId", "collectionId", "key", "required"] + } + }, + { + name: "list_attributes", + description: "List all attributes in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" } + }, + required: ["databaseId", "collectionId"] + } + }, + { + name: "delete_attribute", + description: "Delete an attribute from a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + key: { type: "string", description: "Attribute key to delete" } + }, + required: ["databaseId", "collectionId", "key"] + } + }, + + // Index Operations + { + name: "create_index", + description: "Create an index in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + key: { type: "string", description: "Index key" }, + type: { type: "string", description: "Index type (key, fulltext, unique)", enum: ["key", "fulltext", "unique"] }, + attributes: { type: "array", description: "Array of attribute keys", items: { type: "string" } } + }, + required: ["databaseId", "collectionId", "key", "type", "attributes"] + } + }, + { + name: "list_indexes", + description: "List all indexes in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" } + }, + required: ["databaseId", "collectionId"] + } + }, + { + name: "delete_index", + description: "Delete an index from a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + key: { type: "string", description: "Index key to delete" } + }, + required: ["databaseId", "collectionId", "key"] + } + }, + + // Document Operations + { + name: "create_document", + description: "Create a new document in a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + documentId: { type: "string", description: "Unique ID for the document (optional)" }, + data: { type: "object", description: "Document data as JSON object" }, + permissions: { type: "array", description: "Document permissions (optional)", items: { type: "string" } } + }, + required: ["databaseId", "collectionId", "data"] + } + }, + { + name: "get_document", + description: "Get a document by ID", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + documentId: { type: "string", description: "ID of the document" } + }, + required: ["databaseId", "collectionId", "documentId"] + } + }, + { + name: "list_documents", + description: "List documents in a collection with optional queries", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + queries: { type: "array", description: "Query filters (optional)", items: { type: "string" } } + }, + required: ["databaseId", "collectionId"] + } + }, + { + name: "update_document", + description: "Update a document", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + documentId: { type: "string", description: "ID of the document" }, + data: { type: "object", description: "Updated document data" }, + permissions: { type: "array", description: "Document permissions (optional)", items: { type: "string" } } + }, + required: ["databaseId", "collectionId", "documentId", "data"] + } + }, + { + name: "delete_document", + description: "Delete a document", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + documentId: { type: "string", description: "ID of the document" } + }, + required: ["databaseId", "collectionId", "documentId"] + } + }, + + // User Management Operations + { + name: "create_user", + description: "Create a new user", + inputSchema: { + type: "object", + properties: { + userId: { type: "string", description: "Unique user ID (optional)" }, + email: { type: "string", description: "User email" }, + password: { type: "string", description: "User password" }, + name: { type: "string", description: "User name (optional)" } + }, + required: ["email", "password"] + } + }, + { + name: "list_users", + description: "List all users", + inputSchema: { + type: "object", + properties: { + queries: { type: "array", description: "Query filters (optional)", items: { type: "string" } } + } + } + }, + { + name: "get_user", + description: "Get a user by ID", + inputSchema: { + type: "object", + properties: { + userId: { type: "string", description: "User ID" } + }, + required: ["userId"] + } + }, + { + name: "update_user_email", + description: "Update user email", + inputSchema: { + type: "object", + properties: { + userId: { type: "string", description: "User ID" }, + email: { type: "string", description: "New email" } + }, + required: ["userId", "email"] + } + }, + { + name: "update_user_name", + description: "Update user name", + inputSchema: { + type: "object", + properties: { + userId: { type: "string", description: "User ID" }, + name: { type: "string", description: "New name" } + }, + required: ["userId", "name"] + } + }, + { + name: "update_user_password", + description: "Update user password", + inputSchema: { + type: "object", + properties: { + userId: { type: "string", description: "User ID" }, + password: { type: "string", description: "New password" } + }, + required: ["userId", "password"] + } + }, + { + name: "delete_user", + description: "Delete a user", + inputSchema: { + type: "object", + properties: { + userId: { type: "string", description: "User ID" } + }, + required: ["userId"] + } + }, + + // Storage Operations + { + name: "create_bucket", + description: "Create a storage bucket", + inputSchema: { + type: "object", + properties: { + bucketId: { type: "string", description: "Unique bucket ID (optional)" }, + name: { type: "string", description: "Bucket name" }, + permissions: { type: "array", description: "Bucket permissions (optional)", items: { type: "string" } }, + fileSecurity: { type: "boolean", description: "Enable file security (optional)" }, + enabled: { type: "boolean", description: "Enable bucket (optional, default true)" } + }, + required: ["name"] + } + }, + { + name: "list_buckets", + description: "List all storage buckets", + inputSchema: { type: "object", properties: {} } + }, + { + name: "get_bucket", + description: "Get a bucket by ID", + inputSchema: { + type: "object", + properties: { + bucketId: { type: "string", description: "Bucket ID" } + }, + required: ["bucketId"] + } + }, + { + name: "update_bucket", + description: "Update a storage bucket", + inputSchema: { + type: "object", + properties: { + bucketId: { type: "string", description: "Bucket ID" }, + name: { type: "string", description: "Bucket name" }, + permissions: { type: "array", description: "Bucket permissions (optional)", items: { type: "string" } }, + fileSecurity: { type: "boolean", description: "Enable file security (optional)" }, + enabled: { type: "boolean", description: "Enable bucket (optional)" } + }, + required: ["bucketId", "name"] + } + }, + { + name: "delete_bucket", + description: "Delete a storage bucket", + inputSchema: { + type: "object", + properties: { + bucketId: { type: "string", description: "Bucket ID" } + }, + required: ["bucketId"] + } + }, + { + name: "list_files", + description: "List files in a storage bucket", + inputSchema: { + type: "object", + properties: { + bucketId: { type: "string", description: "Bucket ID" } + }, + required: ["bucketId"] + } + }, + + // Function Operations + { + name: "create_function", + description: "Create a new function", + inputSchema: { + type: "object", + properties: { + functionId: { type: "string", description: "Unique function ID (optional)" }, + name: { type: "string", description: "Function name" }, + runtime: { type: "string", description: "Function runtime (e.g., node-18.0, python-3.9)" }, + execute: { type: "array", description: "Execute permissions (optional)", items: { type: "string" } }, + events: { type: "array", description: "Events that trigger the function (optional)", items: { type: "string" } }, + schedule: { type: "string", description: "CRON schedule (optional)" }, + timeout: { type: "number", description: "Timeout in seconds (optional, max 900)" } + }, + required: ["name", "runtime"] + } + }, + { + name: "list_functions", + description: "List all functions", + inputSchema: { type: "object", properties: {} } + }, + { + name: "get_function", + description: "Get a function by ID", + inputSchema: { + type: "object", + properties: { + functionId: { type: "string", description: "Function ID" } + }, + required: ["functionId"] + } + }, + { + name: "update_function", + description: "Update a function", + inputSchema: { + type: "object", + properties: { + functionId: { type: "string", description: "Function ID" }, + name: { type: "string", description: "Function name" }, + runtime: { type: "string", description: "Function runtime (optional)" }, + execute: { type: "array", description: "Execute permissions (optional)", items: { type: "string" } }, + events: { type: "array", description: "Events that trigger the function (optional)", items: { type: "string" } }, + schedule: { type: "string", description: "CRON schedule (optional)" }, + timeout: { type: "number", description: "Timeout in seconds (optional)" } + }, + required: ["functionId", "name"] + } + }, + { + name: "delete_function", + description: "Delete a function", + inputSchema: { + type: "object", + properties: { + functionId: { type: "string", description: "Function ID" } + }, + required: ["functionId"] + } + }, + + // Team Operations + { + name: "create_team", + description: "Create a new team", + inputSchema: { + type: "object", + properties: { + teamId: { type: "string", description: "Unique team ID (optional)" }, + name: { type: "string", description: "Team name" }, + roles: { type: "array", description: "Team roles (optional)", items: { type: "string" } } + }, + required: ["name"] + } + }, + { + name: "list_teams", + description: "List all teams", + inputSchema: { type: "object", properties: {} } + }, + { + name: "get_team", + description: "Get a team by ID", + inputSchema: { + type: "object", + properties: { + teamId: { type: "string", description: "Team ID" } + }, + required: ["teamId"] + } + }, + { + name: "update_team", + description: "Update a team", + inputSchema: { + type: "object", + properties: { + teamId: { type: "string", description: "Team ID" }, + name: { type: "string", description: "Team name" } + }, + required: ["teamId", "name"] + } + }, + { + name: "delete_team", + description: "Delete a team", + inputSchema: { + type: "object", + properties: { + teamId: { type: "string", description: "Team ID" } + }, + required: ["teamId"] + } + }, + + // Smart Schema Operations + { + name: "auto_detect_schema", + description: "Analyze sample data and automatically create collection schema with appropriate attributes", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionName: { type: "string", description: "Name for the new collection" }, + sampleData: { type: "array", description: "Array of sample documents to analyze", items: { type: "object" } }, + collectionId: { type: "string", description: "Custom collection ID (optional)" } + }, + required: ["databaseId", "collectionName", "sampleData"] + } + }, + { + name: "suggest_indexes", + description: "Analyze collection usage patterns and recommend optimal indexes", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + queryPatterns: { type: "array", description: "Common query patterns (optional)", items: { type: "string" } } + }, + required: ["databaseId", "collectionId"] + } + }, + { + name: "validate_document", + description: "Validate document data against collection schema before creation", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + documentData: { type: "object", description: "Document data to validate" } + }, + required: ["databaseId", "collectionId", "documentData"] + } + }, + { + name: "schema_migration", + description: "Automatically migrate collection schema with data preservation", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + newSchema: { type: "array", description: "New schema definition", items: { type: "object" } }, + migrationStrategy: { type: "string", description: "Migration strategy (safe, aggressive)", enum: ["safe", "aggressive"], default: "safe" } + }, + required: ["databaseId", "collectionId", "newSchema"] + } + }, + + + // Data Analysis & Insights + { + name: "analyze_collection", + description: "Get comprehensive data insights and patterns from a collection", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + analysisType: { type: "string", description: "Type of analysis", enum: ["basic", "detailed", "statistical"], default: "basic" } + }, + required: ["databaseId", "collectionId"] + } + }, + { + name: "detect_duplicates", + description: "Find potential duplicate records across collections", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + fields: { type: "array", description: "Fields to check for duplicates", items: { type: "string" } }, + threshold: { type: "number", description: "Similarity threshold (0-1)", default: 0.9 } + }, + required: ["databaseId", "collectionId", "fields"] + } + }, + { + name: "data_quality_check", + description: "Analyze data quality and identify issues (missing fields, invalid formats, etc.)", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection" }, + checkType: { type: "string", description: "Type of quality check", enum: ["completeness", "validity", "consistency", "all"], default: "all" } + }, + required: ["databaseId", "collectionId"] + } + }, + { + name: "usage_stats", + description: "Get usage statistics and access patterns for collections and documents", + inputSchema: { + type: "object", + properties: { + databaseId: { type: "string", description: "ID of the database" }, + collectionId: { type: "string", description: "ID of the collection (optional, for all collections if not specified)" }, + timeRange: { type: "string", description: "Time range for stats", enum: ["1d", "7d", "30d", "90d"], default: "7d" } + }, + required: ["databaseId"] + } + } + ] + }; + }); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (!this.config || !this.databases) { + throw new McpError( + ErrorCode.InternalError, + "Appwrite not configured. Please check your appwrite-config.json file." + ); + } + + try { + switch (request.params.name) { + // Database Operations + case "create_database": + return await this.createDatabase(request.params.arguments); + case "list_databases": + return await this.listDatabases(); + case "delete_database": + return await this.deleteDatabase(request.params.arguments); + + // Collection Operations + case "create_collection": + return await this.createCollection(request.params.arguments); + case "list_collections": + return await this.listCollections(request.params.arguments); + case "delete_collection": + return await this.deleteCollection(request.params.arguments); + + // Attribute Operations + case "create_string_attribute": + return await this.createStringAttribute(request.params.arguments); + case "create_integer_attribute": + return await this.createIntegerAttribute(request.params.arguments); + case "create_boolean_attribute": + return await this.createBooleanAttribute(request.params.arguments); + case "create_email_attribute": + return await this.createEmailAttribute(request.params.arguments); + case "create_datetime_attribute": + return await this.createDatetimeAttribute(request.params.arguments); + case "list_attributes": + return await this.listAttributes(request.params.arguments); + case "delete_attribute": + return await this.deleteAttribute(request.params.arguments); + + // Index Operations + case "create_index": + return await this.createIndex(request.params.arguments); + case "list_indexes": + return await this.listIndexes(request.params.arguments); + case "delete_index": + return await this.deleteIndex(request.params.arguments); + + // Document Operations + case "create_document": + return await this.createDocument(request.params.arguments); + case "get_document": + return await this.getDocument(request.params.arguments); + case "list_documents": + return await this.listDocuments(request.params.arguments); + case "update_document": + return await this.updateDocument(request.params.arguments); + case "delete_document": + return await this.deleteDocument(request.params.arguments); + + // User Management Operations + case "create_user": + return await this.createUser(request.params.arguments); + case "list_users": + return await this.listUsers(request.params.arguments); + case "get_user": + return await this.getUser(request.params.arguments); + case "update_user_email": + return await this.updateUserEmail(request.params.arguments); + case "update_user_name": + return await this.updateUserName(request.params.arguments); + case "update_user_password": + return await this.updateUserPassword(request.params.arguments); + case "delete_user": + return await this.deleteUser(request.params.arguments); + + // Storage Operations + case "create_bucket": + return await this.createBucket(request.params.arguments); + case "list_buckets": + return await this.listBuckets(); + case "get_bucket": + return await this.getBucket(request.params.arguments); + case "update_bucket": + return await this.updateBucket(request.params.arguments); + case "delete_bucket": + return await this.deleteBucket(request.params.arguments); + case "list_files": + return await this.listFiles(request.params.arguments); + + // Function Operations + case "create_function": + return await this.createFunction(request.params.arguments); + case "list_functions": + return await this.listFunctions(); + case "get_function": + return await this.getFunction(request.params.arguments); + case "update_function": + return await this.updateFunction(request.params.arguments); + case "delete_function": + return await this.deleteFunction(request.params.arguments); + + // Team Operations + case "create_team": + return await this.createTeam(request.params.arguments); + case "list_teams": + return await this.listTeams(); + case "get_team": + return await this.getTeam(request.params.arguments); + case "update_team": + return await this.updateTeam(request.params.arguments); + case "delete_team": + return await this.deleteTeam(request.params.arguments); + + // Smart Schema Operations + case "auto_detect_schema": + return await this.autoDetectSchema(request.params.arguments); + case "suggest_indexes": + return await this.suggestIndexes(request.params.arguments); + case "validate_document": + return await this.validateDocument(request.params.arguments); + case "schema_migration": + return await this.schemaMigration(request.params.arguments); + + + // Data Analysis & Insights + case "analyze_collection": + return await this.analyzeCollection(request.params.arguments); + case "detect_duplicates": + return await this.detectDuplicates(request.params.arguments); + case "data_quality_check": + return await this.dataQualityCheck(request.params.arguments); + case "usage_stats": + return await this.usageStats(request.params.arguments); + + default: + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown tool: ${request.params.name}` + ); + } + } catch (error) { + throw new McpError( + ErrorCode.InternalError, + `Appwrite operation failed: ${error instanceof Error ? error.message : String(error)}` + ); + } + }); + } + + private async createDatabase(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const databaseId = args.databaseId || ID.unique(); + const name = args.name; + + const database = await this.databases.create(databaseId, name); + + return { + content: [ + { + type: "text", + text: `Database created successfully:\n- ID: ${database.$id}\n- Name: ${database.name}\n- Created: ${database.$createdAt}` + } + ] + }; + } + + private async listDatabases() { + if (!this.databases) throw new Error("Databases not initialized"); + + const databases = await this.databases.list(); + + const databaseList = databases.databases.map(db => + `- ${db.name} (ID: ${db.$id})` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Databases (${databases.total}):\n${databaseList}` + } + ] + }; + } + + private async deleteDatabase(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const databaseId = args.databaseId; + + await this.databases.delete(databaseId); + + return { + content: [ + { + type: "text", + text: `Database ${databaseId} deleted successfully` + } + ] + }; + } + + private async createCollection(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const databaseId = args.databaseId; + const collectionId = args.collectionId || ID.unique(); + const name = args.name; + + const collection = await this.databases.createCollection(databaseId, collectionId, name); + + return { + content: [ + { + type: "text", + text: `Collection created successfully:\n- ID: ${collection.$id}\n- Name: ${collection.name}\n- Database: ${collection.databaseId}\n- Created: ${collection.$createdAt}` + } + ] + }; + } + + private async listCollections(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const databaseId = args.databaseId; + + const collections = await this.databases.listCollections(databaseId); + + const collectionList = collections.collections.map(col => + `- ${col.name} (ID: ${col.$id})` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Collections in database ${databaseId} (${collections.total}):\n${collectionList}` + } + ] + }; + } + + private async deleteCollection(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const databaseId = args.databaseId; + const collectionId = args.collectionId; + + await this.databases.deleteCollection(databaseId, collectionId); + + return { + content: [ + { + type: "text", + text: `Collection ${collectionId} deleted successfully from database ${databaseId}` + } + ] + }; + } + + // Attribute Operations + private async createStringAttribute(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, key, size, required, default: defaultValue } = args; + + const attribute = await this.databases.createStringAttribute( + databaseId, + collectionId, + key, + size, + required, + defaultValue + ); + + return { + content: [ + { + type: "text", + text: `String attribute '${key}' created successfully in collection ${collectionId}` + } + ] + }; + } + + private async createIntegerAttribute(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, key, required, min, max, default: defaultValue } = args; + + const attribute = await this.databases.createIntegerAttribute( + databaseId, + collectionId, + key, + required, + min, + max, + defaultValue + ); + + return { + content: [ + { + type: "text", + text: `Integer attribute '${key}' created successfully in collection ${collectionId}` + } + ] + }; + } + + private async createBooleanAttribute(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, key, required, default: defaultValue } = args; + + const attribute = await this.databases.createBooleanAttribute( + databaseId, + collectionId, + key, + required, + defaultValue + ); + + return { + content: [ + { + type: "text", + text: `Boolean attribute '${key}' created successfully in collection ${collectionId}` + } + ] + }; + } + + private async createEmailAttribute(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, key, required, default: defaultValue } = args; + + const attribute = await this.databases.createEmailAttribute( + databaseId, + collectionId, + key, + required, + defaultValue + ); + + return { + content: [ + { + type: "text", + text: `Email attribute '${key}' created successfully in collection ${collectionId}` + } + ] + }; + } + + private async createDatetimeAttribute(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, key, required, default: defaultValue } = args; + + const attribute = await this.databases.createDatetimeAttribute( + databaseId, + collectionId, + key, + required, + defaultValue + ); + + return { + content: [ + { + type: "text", + text: `Datetime attribute '${key}' created successfully in collection ${collectionId}` + } + ] + }; + } + + private async listAttributes(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId } = args; + + const attributes = await this.databases.listAttributes(databaseId, collectionId); + + const attributeList = attributes.attributes.map((attr: any) => + `- ${attr.key} (${attr.type}) - Required: ${attr.required}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Attributes in collection ${collectionId} (${attributes.total}):\n${attributeList}` + } + ] + }; + } + + private async deleteAttribute(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, key } = args; + + await this.databases.deleteAttribute(databaseId, collectionId, key); + + return { + content: [ + { + type: "text", + text: `Attribute '${key}' deleted successfully from collection ${collectionId}` + } + ] + }; + } + + // Index Operations + private async createIndex(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, key, type, attributes } = args; + + const index = await this.databases.createIndex(databaseId, collectionId, key, type, attributes); + + return { + content: [ + { + type: "text", + text: `Index '${key}' created successfully in collection ${collectionId}` + } + ] + }; + } + + private async listIndexes(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId } = args; + + const indexes = await this.databases.listIndexes(databaseId, collectionId); + + const indexList = indexes.indexes.map((idx: any) => + `- ${idx.key} (${idx.type}) - Attributes: ${idx.attributes.join(', ')}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Indexes in collection ${collectionId} (${indexes.total}):\n${indexList}` + } + ] + }; + } + + private async deleteIndex(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, key } = args; + + await this.databases.deleteIndex(databaseId, collectionId, key); + + return { + content: [ + { + type: "text", + text: `Index '${key}' deleted successfully from collection ${collectionId}` + } + ] + }; + } + + // Document Operations + private async createDocument(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, documentId, data, permissions } = args; + const docId = documentId || ID.unique(); + + const document = await this.databases.createDocument( + databaseId, + collectionId, + docId, + data, + permissions + ); + + return { + content: [ + { + type: "text", + text: `Document created successfully:\n- ID: ${document.$id}\n- Collection: ${document.$collectionId}\n- Created: ${document.$createdAt}\n- Data: ${JSON.stringify(data, null, 2)}` + } + ] + }; + } + + private async getDocument(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, documentId } = args; + + const document = await this.databases.getDocument(databaseId, collectionId, documentId); + + return { + content: [ + { + type: "text", + text: `Document ${documentId}:\n${JSON.stringify(document, null, 2)}` + } + ] + }; + } + + private async listDocuments(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, queries } = args; + + const documents = await this.databases.listDocuments(databaseId, collectionId, queries || []); + + return { + content: [ + { + type: "text", + text: `Documents in collection ${collectionId} (${documents.total}):\n${JSON.stringify(documents.documents, null, 2)}` + } + ] + }; + } + + private async updateDocument(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, documentId, data, permissions } = args; + + const document = await this.databases.updateDocument( + databaseId, + collectionId, + documentId, + data, + permissions + ); + + return { + content: [ + { + type: "text", + text: `Document ${documentId} updated successfully:\n${JSON.stringify(document, null, 2)}` + } + ] + }; + } + + private async deleteDocument(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, documentId } = args; + + await this.databases.deleteDocument(databaseId, collectionId, documentId); + + return { + content: [ + { + type: "text", + text: `Document ${documentId} deleted successfully from collection ${collectionId}` + } + ] + }; + } + + // User Management Operations + private async createUser(args: any) { + if (!this.users) throw new Error("Users service not initialized"); + + const { userId, email, password, name } = args; + const uid = userId || ID.unique(); + + const user = await this.users.create(uid, email, password, name); + + return { + content: [ + { + type: "text", + text: `User created successfully:\n- ID: ${user.$id}\n- Email: ${user.email}\n- Name: ${user.name}\n- Created: ${user.$createdAt}` + } + ] + }; + } + + private async listUsers(args: any) { + if (!this.users) throw new Error("Users service not initialized"); + + const { queries } = args; + + const users = await this.users.list(queries || []); + + const userList = users.users.map((user: any) => + `- ${user.name || 'No name'} (${user.email}) - ID: ${user.$id}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Users (${users.total}):\n${userList}` + } + ] + }; + } + + private async getUser(args: any) { + if (!this.users) throw new Error("Users service not initialized"); + + const { userId } = args; + + const user = await this.users.get(userId); + + return { + content: [ + { + type: "text", + text: `User ${userId}:\n${JSON.stringify(user, null, 2)}` + } + ] + }; + } + + private async updateUserEmail(args: any) { + if (!this.users) throw new Error("Users service not initialized"); + + const { userId, email } = args; + + const user = await this.users.updateEmail(userId, email); + + return { + content: [ + { + type: "text", + text: `User ${userId} email updated to: ${email}` + } + ] + }; + } + + private async updateUserName(args: any) { + if (!this.users) throw new Error("Users service not initialized"); + + const { userId, name } = args; + + const user = await this.users.updateName(userId, name); + + return { + content: [ + { + type: "text", + text: `User ${userId} name updated to: ${name}` + } + ] + }; + } + + private async updateUserPassword(args: any) { + if (!this.users) throw new Error("Users service not initialized"); + + const { userId, password } = args; + + const user = await this.users.updatePassword(userId, password); + + return { + content: [ + { + type: "text", + text: `User ${userId} password updated successfully` + } + ] + }; + } + + private async deleteUser(args: any) { + if (!this.users) throw new Error("Users service not initialized"); + + const { userId } = args; + + await this.users.delete(userId); + + return { + content: [ + { + type: "text", + text: `User ${userId} deleted successfully` + } + ] + }; + } + + // Storage Operations + private async createBucket(args: any) { + if (!this.storage) throw new Error("Storage service not initialized"); + + const { bucketId, name, permissions, fileSecurity, enabled } = args; + const bid = bucketId || ID.unique(); + + const bucket = await this.storage.createBucket(bid, name, permissions, fileSecurity, enabled); + + return { + content: [ + { + type: "text", + text: `Bucket created successfully:\n- ID: ${bucket.$id}\n- Name: ${bucket.name}\n- Created: ${bucket.$createdAt}` + } + ] + }; + } + + private async listBuckets() { + if (!this.storage) throw new Error("Storage service not initialized"); + + const buckets = await this.storage.listBuckets(); + + const bucketList = buckets.buckets.map((bucket: any) => + `- ${bucket.name} (ID: ${bucket.$id})` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Storage buckets (${buckets.total}):\n${bucketList}` + } + ] + }; + } + + private async getBucket(args: any) { + if (!this.storage) throw new Error("Storage service not initialized"); + + const { bucketId } = args; + + const bucket = await this.storage.getBucket(bucketId); + + return { + content: [ + { + type: "text", + text: `Bucket ${bucketId}:\n${JSON.stringify(bucket, null, 2)}` + } + ] + }; + } + + private async updateBucket(args: any) { + if (!this.storage) throw new Error("Storage service not initialized"); + + const { bucketId, name, permissions, fileSecurity, enabled } = args; + + const bucket = await this.storage.updateBucket(bucketId, name, permissions, fileSecurity, enabled); + + return { + content: [ + { + type: "text", + text: `Bucket ${bucketId} updated successfully` + } + ] + }; + } + + private async deleteBucket(args: any) { + if (!this.storage) throw new Error("Storage service not initialized"); + + const { bucketId } = args; + + await this.storage.deleteBucket(bucketId); + + return { + content: [ + { + type: "text", + text: `Bucket ${bucketId} deleted successfully` + } + ] + }; + } + + private async listFiles(args: any) { + if (!this.storage) throw new Error("Storage service not initialized"); + + const { bucketId } = args; + + const files = await this.storage.listFiles(bucketId); + + const fileList = files.files.map((file: any) => + `- ${file.name} (${file.sizeOriginal} bytes) - ID: ${file.$id}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Files in bucket ${bucketId} (${files.total}):\n${fileList}` + } + ] + }; + } + + // Function Operations + private async createFunction(args: any) { + if (!this.functions) throw new Error("Functions service not initialized"); + + const { functionId, name, runtime, execute, events, schedule, timeout } = args; + const fid = functionId || ID.unique(); + + const func = await this.functions.create(fid, name, runtime, execute, events, schedule, timeout); + + return { + content: [ + { + type: "text", + text: `Function created successfully:\n- ID: ${func.$id}\n- Name: ${func.name}\n- Runtime: ${func.runtime}\n- Created: ${func.$createdAt}` + } + ] + }; + } + + private async listFunctions() { + if (!this.functions) throw new Error("Functions service not initialized"); + + const functions = await this.functions.list(); + + const functionList = functions.functions.map((func: any) => + `- ${func.name} (${func.runtime}) - ID: ${func.$id}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Functions (${functions.total}):\n${functionList}` + } + ] + }; + } + + private async getFunction(args: any) { + if (!this.functions) throw new Error("Functions service not initialized"); + + const { functionId } = args; + + const func = await this.functions.get(functionId); + + return { + content: [ + { + type: "text", + text: `Function ${functionId}:\n${JSON.stringify(func, null, 2)}` + } + ] + }; + } + + private async updateFunction(args: any) { + if (!this.functions) throw new Error("Functions service not initialized"); + + const { functionId, name, runtime, execute, events, schedule, timeout } = args; + + const func = await this.functions.update(functionId, name, runtime, execute, events, schedule, timeout); + + return { + content: [ + { + type: "text", + text: `Function ${functionId} updated successfully` + } + ] + }; + } + + private async deleteFunction(args: any) { + if (!this.functions) throw new Error("Functions service not initialized"); + + const { functionId } = args; + + await this.functions.delete(functionId); + + return { + content: [ + { + type: "text", + text: `Function ${functionId} deleted successfully` + } + ] + }; + } + + // Team Operations + private async createTeam(args: any) { + if (!this.teams) throw new Error("Teams service not initialized"); + + const { teamId, name, roles } = args; + const tid = teamId || ID.unique(); + + const team = await this.teams.create(tid, name, roles); + + return { + content: [ + { + type: "text", + text: `Team created successfully:\n- ID: ${team.$id}\n- Name: ${team.name}\n- Created: ${team.$createdAt}` + } + ] + }; + } + + private async listTeams() { + if (!this.teams) throw new Error("Teams service not initialized"); + + const teams = await this.teams.list(); + + const teamList = teams.teams.map((team: any) => + `- ${team.name} - ID: ${team.$id}` + ).join('\n'); + + return { + content: [ + { + type: "text", + text: `Teams (${teams.total}):\n${teamList}` + } + ] + }; + } + + private async getTeam(args: any) { + if (!this.teams) throw new Error("Teams service not initialized"); + + const { teamId } = args; + + const team = await this.teams.get(teamId); + + return { + content: [ + { + type: "text", + text: `Team ${teamId}:\n${JSON.stringify(team, null, 2)}` + } + ] + }; + } + + private async updateTeam(args: any) { + if (!this.teams) throw new Error("Teams service not initialized"); + + const { teamId, name } = args; + + const team = await this.teams.updateName(teamId, name); + + return { + content: [ + { + type: "text", + text: `Team ${teamId} name updated to: ${name}` + } + ] + }; + } + + private async deleteTeam(args: any) { + if (!this.teams) throw new Error("Teams service not initialized"); + + const { teamId } = args; + + await this.teams.delete(teamId); + + return { + content: [ + { + type: "text", + text: `Team ${teamId} deleted successfully` + } + ] + }; + } + + // Smart Schema Operations + private async autoDetectSchema(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionName, sampleData, collectionId } = args; + const cid = collectionId || ID.unique(); + + // Analyze sample data to determine schema + const schema = this.analyzeDataStructure(sampleData); + + // Create collection + const collection = await this.databases.createCollection(databaseId, cid, collectionName); + + // Create attributes based on detected schema + const createdAttributes = []; + for (const attr of schema.attributes) { + try { + switch (attr.type) { + case 'string': + await this.databases.createStringAttribute( + databaseId, cid, attr.key, attr.size || 255, attr.required || false + ); + break; + case 'integer': + await this.databases.createIntegerAttribute( + databaseId, cid, attr.key, attr.required || false + ); + break; + case 'boolean': + await this.databases.createBooleanAttribute( + databaseId, cid, attr.key, attr.required || false + ); + break; + case 'email': + await this.databases.createEmailAttribute( + databaseId, cid, attr.key, attr.required || false + ); + break; + case 'datetime': + await this.databases.createDatetimeAttribute( + databaseId, cid, attr.key, attr.required || false + ); + break; + } + createdAttributes.push(attr); + } catch (error) { + console.error(`Failed to create attribute ${attr.key}:`, error); + } + } + + // Suggest indexes for commonly queried fields + const suggestedIndexes = schema.suggestedIndexes; + + return { + content: [ + { + type: "text", + text: `Schema auto-detected and collection created successfully: +- Collection: ${collectionName} (ID: ${cid}) +- Attributes created: ${createdAttributes.length} +- Detected types: ${createdAttributes.map(a => `${a.key} (${a.type})`).join(', ')} +- Suggested indexes: ${suggestedIndexes.join(', ')} + +Schema Analysis: +${JSON.stringify(schema, null, 2)}` + } + ] + }; + } + + private analyzeDataStructure(sampleData: any[]): any { + const fieldTypes: { [key: string]: { type: string; required: boolean; size?: number; samples: any[] } } = {}; + const totalRecords = sampleData.length; + + // Analyze each sample document + sampleData.forEach(doc => { + Object.keys(doc).forEach(key => { + if (!fieldTypes[key]) { + fieldTypes[key] = { type: 'unknown', required: false, samples: [] }; + } + fieldTypes[key].samples.push(doc[key]); + }); + }); + + // Determine types and properties + const attributes = Object.keys(fieldTypes).map(key => { + const field = fieldTypes[key]; + const samples = field.samples; + const nonNullSamples = samples.filter(s => s !== null && s !== undefined); + + // Calculate required percentage + const requiredPercentage = nonNullSamples.length / totalRecords; + const required = requiredPercentage > 0.8; // 80% threshold for required + + // Detect type + let type = 'string'; + let size = 255; + + if (nonNullSamples.length > 0) { + const firstSample = nonNullSamples[0]; + + if (typeof firstSample === 'boolean') { + type = 'boolean'; + } else if (typeof firstSample === 'number' && Number.isInteger(firstSample)) { + type = 'integer'; + } else if (typeof firstSample === 'string') { + // Check for email pattern + if (nonNullSamples.some(s => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s))) { + type = 'email'; + } + // Check for datetime pattern + else if (nonNullSamples.some(s => !isNaN(Date.parse(s)))) { + type = 'datetime'; + } else { + type = 'string'; + // Calculate max string length + const maxLength = Math.max(...nonNullSamples.map(s => String(s).length)); + size = Math.max(255, Math.ceil(maxLength * 1.2)); // 20% buffer + } + } + } + + return { key, type, required, size: type === 'string' ? size : undefined }; + }); + + // Suggest indexes for common query fields + const suggestedIndexes = attributes + .filter(attr => ['email', 'string'].includes(attr.type) && attr.key.match(/(id|email|name|slug|status|type)/i)) + .map(attr => attr.key); + + return { attributes, suggestedIndexes, totalRecords, analysis: fieldTypes }; + } + + private async suggestIndexes(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, queryPatterns } = args; + + // Get collection attributes + const attributes = await this.databases.listAttributes(databaseId, collectionId); + const attributeMap = new Map(attributes.attributes.map((attr: any) => [attr.key, attr])); + + // Get existing indexes + const existingIndexes = await this.databases.listIndexes(databaseId, collectionId); + const existingIndexKeys = new Set(existingIndexes.indexes.map((idx: any) => idx.key)); + + const suggestions: any[] = []; + + // Standard suggestions based on attribute types + attributes.attributes.forEach((attr: any) => { + const indexKey = `idx_${attr.key}`; + if (existingIndexKeys.has(indexKey)) return; + + if (attr.type === 'email' || attr.key.includes('email')) { + suggestions.push({ + key: indexKey, + type: 'unique', + attributes: [attr.key], + reason: 'Email fields should have unique indexes for fast lookups and uniqueness constraints' + }); + } else if (attr.key.match(/(id|name|slug|status|type|category)/i)) { + suggestions.push({ + key: indexKey, + type: 'key', + attributes: [attr.key], + reason: `Common query field '${attr.key}' would benefit from indexing` + }); + } else if (attr.type === 'string' && attr.key.match(/(title|description|content|body)/i)) { + suggestions.push({ + key: `fulltext_${attr.key}`, + type: 'fulltext', + attributes: [attr.key], + reason: `Text field '${attr.key}' would benefit from full-text search indexing` + }); + } + }); + + // Analyze query patterns if provided + if (queryPatterns && queryPatterns.length > 0) { + queryPatterns.forEach((pattern: string, index: number) => { + const fields = this.extractFieldsFromQuery(pattern); + if (fields.length > 1) { + const compositeKey = `composite_${fields.join('_')}`; + if (!existingIndexKeys.has(compositeKey)) { + suggestions.push({ + key: compositeKey, + type: 'key', + attributes: fields, + reason: `Composite index for query pattern: "${pattern}"` + }); + } + } + }); + } + + // Remove duplicates and limit suggestions + const uniqueSuggestions = suggestions.slice(0, 10); + + return { + content: [ + { + type: "text", + text: `Index Recommendations for Collection ${collectionId}: + +Found ${attributes.total} attributes, ${existingIndexes.total} existing indexes + +Recommended Indexes (${uniqueSuggestions.length}): +${uniqueSuggestions.map(s => + `โ€ข ${s.key} (${s.type}) on [${s.attributes.join(', ')}] + Reason: ${s.reason}` +).join('\n\n')} + +Existing Indexes: +${existingIndexes.indexes.map((idx: any) => `โ€ข ${idx.key} (${idx.type}) on [${idx.attributes.join(', ')}]`).join('\n')} + +To create recommended indexes, use the create_index tool with the suggested parameters.` + } + ] + }; + } + + private extractFieldsFromQuery(query: string): string[] { + // Simple pattern extraction - look for field names in common query patterns + const patterns = [ + /Query\.equal\(['"]([^'"]+)['"]/g, + /Query\.search\(['"]([^'"]+)['"]/g, + /Query\.orderBy\(['"]([^'"]+)['"]/g, + /where\s+(\w+)\s*=/gi, + /order\s+by\s+(\w+)/gi, + /group\s+by\s+(\w+)/gi + ]; + + const fields = new Set(); + patterns.forEach(pattern => { + let match; + while ((match = pattern.exec(query)) !== null) { + fields.add(match[1]); + } + }); + + return Array.from(fields); + } + + private async validateDocument(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, documentData } = args; + + // Get collection schema + const attributes = await this.databases.listAttributes(databaseId, collectionId); + const attributeMap = new Map(attributes.attributes.map((attr: any) => [attr.key, attr])); + + const errors: string[] = []; + const warnings: string[] = []; + const valid = true; + + // Check required fields + attributes.attributes.forEach((attr: any) => { + if (attr.required && !(attr.key in documentData)) { + errors.push(`Missing required field: ${attr.key} (${attr.type})`); + } + }); + + // Check field types and constraints + Object.keys(documentData).forEach(key => { + const attr = attributeMap.get(key); + const value = documentData[key]; + + if (!attr) { + warnings.push(`Unknown field: ${key} (not in collection schema)`); + return; + } + + // Type validation + switch (attr.type) { + case 'string': + if (typeof value !== 'string') { + errors.push(`Field ${key}: expected string, got ${typeof value}`); + } else if (attr.size && value.length > attr.size) { + errors.push(`Field ${key}: string too long (${value.length} > ${attr.size})`); + } + break; + case 'integer': + if (!Number.isInteger(value)) { + errors.push(`Field ${key}: expected integer, got ${typeof value}`); + } + break; + case 'boolean': + if (typeof value !== 'boolean') { + errors.push(`Field ${key}: expected boolean, got ${typeof value}`); + } + break; + case 'email': + if (typeof value !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { + errors.push(`Field ${key}: invalid email format`); + } + break; + case 'datetime': + if (isNaN(Date.parse(value))) { + errors.push(`Field ${key}: invalid datetime format`); + } + break; + } + }); + + const isValid = errors.length === 0; + + return { + content: [ + { + type: "text", + text: `Document Validation Result: + +Status: ${isValid ? 'โœ… VALID' : 'โŒ INVALID'} + +${errors.length > 0 ? `Errors (${errors.length}): +${errors.map(e => `โ€ข ${e}`).join('\n')} + +` : ''}${warnings.length > 0 ? `Warnings (${warnings.length}): +${warnings.map(w => `โ€ข ${w}`).join('\n')} + +` : ''}Document Data: +${JSON.stringify(documentData, null, 2)} + +Collection Schema: +${attributes.attributes.map((attr: any) => + `โ€ข ${attr.key} (${attr.type}) - Required: ${attr.required}${attr.size ? `, Max size: ${attr.size}` : ''}` +).join('\n')}` + } + ] + }; + } + + private async schemaMigration(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, newSchema, migrationStrategy = 'safe' } = args; + + // Get current schema + const currentAttributes = await this.databases.listAttributes(databaseId, collectionId); + const currentAttrMap = new Map(currentAttributes.attributes.map((attr: any) => [attr.key, attr])); + + const migration = { + toAdd: [] as any[], + toRemove: [] as any[], + toModify: [] as any[], + warnings: [] as string[] + }; + + // Analyze differences + newSchema.forEach((newAttr: any) => { + const current = currentAttrMap.get(newAttr.key); + if (!current) { + migration.toAdd.push(newAttr); + } else if (current.type !== newAttr.type) { + migration.toModify.push({ + key: newAttr.key, + currentType: current.type, + newType: newAttr.type, + action: 'type_change' + }); + migration.warnings.push(`Type change for ${newAttr.key}: ${current.type} โ†’ ${newAttr.type} may cause data loss`); + } + }); + + // Find attributes to remove + currentAttributes.attributes.forEach((currentAttr: any) => { + if (!newSchema.find((newAttr: any) => newAttr.key === currentAttr.key)) { + migration.toRemove.push(currentAttr); + if (migrationStrategy === 'safe') { + migration.warnings.push(`Attribute ${currentAttr.key} will be removed - this cannot be undone`); + } + } + }); + + let migrationResult = ''; + + if (migrationStrategy === 'safe' && (migration.toRemove.length > 0 || migration.toModify.length > 0)) { + return { + content: [ + { + type: "text", + text: `Schema Migration Analysis (Safe Mode): + +โš ๏ธ MIGRATION BLOCKED - Potentially destructive changes detected: + +To Add (${migration.toAdd.length}): +${migration.toAdd.map(attr => `โ€ข ${attr.key} (${attr.type})`).join('\n')} + +To Remove (${migration.toRemove.length}): +${migration.toRemove.map(attr => `โ€ข ${attr.key} (${attr.type})`).join('\n')} + +To Modify (${migration.toModify.length}): +${migration.toModify.map(attr => `โ€ข ${attr.key}: ${attr.currentType} โ†’ ${attr.newType}`).join('\n')} + +Warnings: +${migration.warnings.map(w => `โ€ข ${w}`).join('\n')} + +To proceed with potentially destructive changes, use migrationStrategy: "aggressive"` + } + ] + }; + } + + // Execute migration + const results = []; + + // Add new attributes + for (const attr of migration.toAdd) { + try { + switch (attr.type) { + case 'string': + await this.databases.createStringAttribute( + databaseId, collectionId, attr.key, attr.size || 255, attr.required || false + ); + break; + case 'integer': + await this.databases.createIntegerAttribute( + databaseId, collectionId, attr.key, attr.required || false + ); + break; + case 'boolean': + await this.databases.createBooleanAttribute( + databaseId, collectionId, attr.key, attr.required || false + ); + break; + case 'email': + await this.databases.createEmailAttribute( + databaseId, collectionId, attr.key, attr.required || false + ); + break; + case 'datetime': + await this.databases.createDatetimeAttribute( + databaseId, collectionId, attr.key, attr.required || false + ); + break; + } + results.push(`โœ… Added ${attr.key} (${attr.type})`); + } catch (error) { + results.push(`โŒ Failed to add ${attr.key}: ${error}`); + } + } + + // Remove attributes (aggressive mode only) + if (migrationStrategy === 'aggressive') { + for (const attr of migration.toRemove) { + try { + await this.databases.deleteAttribute(databaseId, collectionId, attr.key); + results.push(`โœ… Removed ${attr.key}`); + } catch (error) { + results.push(`โŒ Failed to remove ${attr.key}: ${error}`); + } + } + } + + return { + content: [ + { + type: "text", + text: `Schema Migration Completed (${migrationStrategy} mode): + +Migration Results: +${results.join('\n')} + +${migration.warnings.length > 0 ? `\nWarnings: +${migration.warnings.map(w => `โ€ข ${w}`).join('\n')}` : ''} + +Summary: +โ€ข Added: ${migration.toAdd.length} attributes +โ€ข Removed: ${migrationStrategy === 'aggressive' ? migration.toRemove.length : 0} attributes +โ€ข Modified: ${migration.toModify.length} attributes` + } + ] + }; + } + + + + + + + // Data Analysis & Insights + private async analyzeCollection(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, analysisType = 'basic' } = args; + + try { + // Get collection schema + const attributes = await this.databases.listAttributes(databaseId, collectionId); + const indexes = await this.databases.listIndexes(databaseId, collectionId); + + // Get sample of documents for analysis + const documents = await this.databases.listDocuments( + databaseId, + collectionId, + [Query.limit(1000)] + ); + + const analysis = this.performCollectionAnalysis(documents.documents, attributes.attributes, analysisType); + + return { + content: [ + { + type: "text", + text: `Collection Analysis: ${collectionId} + +๐Ÿ“Š Basic Statistics: +โ€ข Total documents: ${documents.total} +โ€ข Attributes: ${attributes.total} +โ€ข Indexes: ${indexes.total} +โ€ข Sample size: ${documents.documents.length} + +๐Ÿ—๏ธ Schema Analysis: +${attributes.attributes.map((attr: any) => + `โ€ข ${attr.key} (${attr.type}) - Required: ${attr.required}${attr.size ? `, Size: ${attr.size}` : ''}` +).join('\n')} + +๐Ÿ“ˆ Data Distribution: +${JSON.stringify(analysis.distribution, null, 2)} + +${analysisType === 'detailed' || analysisType === 'statistical' ? ` + +๐Ÿ” Detailed Insights: +${JSON.stringify(analysis.insights, null, 2)}` : ''} + +${analysisType === 'statistical' ? ` + +๐Ÿ“Š Statistical Analysis: +${JSON.stringify(analysis.statistics, null, 2)}` : ''} + +๐Ÿ’ก Recommendations: +${analysis.recommendations.map((rec: string) => `โ€ข ${rec}`).join('\n')}` + } + ] + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Collection Analysis Failed: + +Error: ${error} + +Please ensure: +โ€ข Collection exists and is accessible +โ€ข You have read permissions +โ€ข Collection is not empty` + } + ] + }; + } + } + + private performCollectionAnalysis(documents: any[], attributes: any[], analysisType: string): any { + const analysis: any = { + distribution: {}, + insights: {}, + statistics: {}, + recommendations: [] + }; + + // Basic distribution analysis + attributes.forEach(attr => { + const values = documents.map(doc => doc[attr.key]).filter(val => val !== null && val !== undefined); + const uniqueValues = [...new Set(values)]; + + analysis.distribution[attr.key] = { + totalValues: values.length, + uniqueValues: uniqueValues.length, + nullPercentage: ((documents.length - values.length) / documents.length * 100).toFixed(1) + '%', + mostCommonValue: this.getMostCommonValue(values) + }; + + // Type-specific analysis + if (attr.type === 'string') { + const lengths = values.map(val => String(val).length); + analysis.distribution[attr.key].avgLength = lengths.reduce((sum, len) => sum + len, 0) / lengths.length; + analysis.distribution[attr.key].maxLength = Math.max(...lengths); + } else if (attr.type === 'integer') { + const numbers = values.filter(val => typeof val === 'number'); + if (numbers.length > 0) { + analysis.distribution[attr.key].min = Math.min(...numbers); + analysis.distribution[attr.key].max = Math.max(...numbers); + analysis.distribution[attr.key].avg = numbers.reduce((sum, num) => sum + num, 0) / numbers.length; + } + } + }); + + // Detailed insights + if (analysisType === 'detailed' || analysisType === 'statistical') { + analysis.insights = { + dataQuality: this.assessDataQuality(documents, attributes), + patterns: this.findDataPatterns(documents), + relationships: this.detectRelationships(documents, attributes) + }; + } + + // Statistical analysis + if (analysisType === 'statistical') { + analysis.statistics = this.calculateStatistics(documents, attributes); + } + + // Generate recommendations + analysis.recommendations = this.generateRecommendations(documents, attributes, analysis); + + return analysis; + } + + private getMostCommonValue(values: any[]): any { + const frequency: { [key: string]: number } = {}; + values.forEach(val => { + const key = String(val); + frequency[key] = (frequency[key] || 0) + 1; + }); + + let mostCommon = null; + let maxCount = 0; + Object.keys(frequency).forEach(key => { + if (frequency[key] > maxCount) { + maxCount = frequency[key]; + mostCommon = key; + } + }); + + return { value: mostCommon, frequency: maxCount }; + } + + private assessDataQuality(documents: any[], attributes: any[]): any { + return { + completeness: attributes.map(attr => ({ + field: attr.key, + completeness: ((documents.filter(doc => doc[attr.key] !== null && doc[attr.key] !== undefined).length / documents.length) * 100).toFixed(1) + '%' + })), + duplicates: this.findDuplicateDocuments(documents) + }; + } + + private findDataPatterns(documents: any[]): any { + return { + commonFields: this.findCommonFieldPatterns(documents), + temporalPatterns: this.findTemporalPatterns(documents) + }; + } + + private detectRelationships(documents: any[], attributes: any[]): any { + return { + foreignKeys: attributes.filter(attr => attr.key.endsWith('Id') || attr.key.endsWith('_id')), + potentialRelations: this.findPotentialRelations(documents, attributes) + }; + } + + private calculateStatistics(documents: any[], attributes: any[]): any { + const stats: any = {}; + + attributes.forEach(attr => { + if (attr.type === 'integer') { + const values = documents.map(doc => doc[attr.key]).filter(val => typeof val === 'number'); + if (values.length > 0) { + const sorted = values.sort((a, b) => a - b); + stats[attr.key] = { + mean: values.reduce((sum, val) => sum + val, 0) / values.length, + median: sorted[Math.floor(sorted.length / 2)], + mode: this.getMostCommonValue(values).value, + standardDeviation: this.calculateStandardDeviation(values) + }; + } + } + }); + + return stats; + } + + private calculateStandardDeviation(values: number[]): number { + const mean = values.reduce((sum, val) => sum + val, 0) / values.length; + const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length; + return Math.sqrt(variance); + } + + private generateRecommendations(documents: any[], attributes: any[], analysis: any): string[] { + const recommendations = []; + + // Index recommendations + attributes.forEach(attr => { + if (attr.key.match(/(id|email|status|type)/i) && analysis.distribution[attr.key]?.uniqueValues > 10) { + recommendations.push(`Consider adding an index on '${attr.key}' for better query performance`); + } + }); + + // Data quality recommendations + Object.keys(analysis.distribution).forEach(field => { + const dist = analysis.distribution[field]; + if (parseFloat(dist.nullPercentage) > 50) { + recommendations.push(`Field '${field}' has high null percentage (${dist.nullPercentage}) - consider making it optional or providing defaults`); + } + }); + + // Schema recommendations + if (attributes.length > 20) { + recommendations.push('Consider splitting this collection into smaller, more focused collections'); + } + + return recommendations; + } + + private findDuplicateDocuments(documents: any[]): any { + // Simple duplicate detection based on multiple fields + const seen = new Map(); + const duplicates: any[] = []; + + documents.forEach((doc, index) => { + const key = JSON.stringify(Object.keys(doc).sort().map(k => doc[k])); + if (seen.has(key)) { + duplicates.push({ index, duplicateOf: seen.get(key) }); + } else { + seen.set(key, index); + } + }); + + return { count: duplicates.length, examples: duplicates.slice(0, 5) }; + } + + private findCommonFieldPatterns(documents: any[]): any { + const patterns: { [key: string]: number } = {}; + + documents.forEach(doc => { + const fields = Object.keys(doc).sort().join(','); + patterns[fields] = (patterns[fields] || 0) + 1; + }); + + return Object.keys(patterns).map(pattern => ({ + pattern, + frequency: patterns[pattern] + })).sort((a, b) => b.frequency - a.frequency).slice(0, 5); + } + + private findTemporalPatterns(documents: any[]): any { + const createdDates = documents + .map(doc => doc.$createdAt) + .filter(date => date) + .map(date => new Date(date)); + + if (createdDates.length === 0) return null; + + const dateGroups: { [key: string]: number } = {}; + createdDates.forEach(date => { + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + dateGroups[monthKey] = (dateGroups[monthKey] || 0) + 1; + }); + + return { + creationPattern: dateGroups, + peakMonth: Object.keys(dateGroups).reduce((a, b) => dateGroups[a] > dateGroups[b] ? a : b) + }; + } + + private findPotentialRelations(documents: any[], attributes: any[]): any { + return attributes.filter(attr => + attr.key.endsWith('Id') || + attr.key.endsWith('_id') || + attr.key.match(/(user|author|category|parent)/i) + ).map(attr => ({ + field: attr.key, + possibleRelation: attr.key.replace(/Id$|_id$/i, ''), + uniqueValues: [...new Set(documents.map(doc => doc[attr.key]))].length + })); + } + + private async detectDuplicates(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, fields, threshold = 0.9 } = args; + + try { + const documents = await this.databases.listDocuments( + databaseId, + collectionId, + [Query.limit(5000)] + ); + + const duplicates = this.findDuplicates(documents.documents, fields, threshold); + + return { + content: [ + { + type: "text", + text: `Duplicate Detection Results: + +Collection: ${collectionId} +Fields analyzed: ${fields.join(', ')} +Similarity threshold: ${threshold} +Documents scanned: ${documents.documents.length} + +๐Ÿ” Duplicate Groups Found: ${duplicates.groups.length} + +${duplicates.groups.slice(0, 10).map((group: any, index: number) => + `Group ${index + 1} (${group.documents.length} duplicates): + Similarity: ${(group.similarity * 100).toFixed(1)}% + Documents: ${group.documents.map((doc: any) => doc.$id).join(', ')} + Common values: ${JSON.stringify(group.commonValues, null, 2)}` +).join('\n\n')} + +๐Ÿ“Š Summary: +โ€ข Total potential duplicates: ${duplicates.totalDuplicates} +โ€ข Exact matches: ${duplicates.exactMatches} +โ€ข Similar matches: ${duplicates.similarMatches} +โ€ข Clean documents: ${documents.documents.length - duplicates.totalDuplicates} + +๐Ÿ’ก Actions: +โ€ข Review duplicate groups manually +โ€ข Consider merging exact matches +โ€ข Add unique constraints to prevent future duplicates` + } + ] + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Duplicate Detection Failed: + +Error: ${error} + +Please ensure: +โ€ข Collection exists and is accessible +โ€ข Specified fields exist in the collection +โ€ข You have read permissions` + } + ] + }; + } + } + + private findDuplicates(documents: any[], fields: string[], threshold: number): any { + const groups = []; + const processed = new Set(); + let totalDuplicates = 0; + let exactMatches = 0; + let similarMatches = 0; + + for (let i = 0; i < documents.length; i++) { + if (processed.has(i)) continue; + + const currentDoc = documents[i]; + const duplicateGroup = [currentDoc]; + processed.add(i); + + for (let j = i + 1; j < documents.length; j++) { + if (processed.has(j)) continue; + + const compareDoc = documents[j]; + const similarity = this.calculateSimilarity(currentDoc, compareDoc, fields); + + if (similarity >= threshold) { + duplicateGroup.push(compareDoc); + processed.add(j); + + if (similarity === 1.0) exactMatches++; + else similarMatches++; + } + } + + if (duplicateGroup.length > 1) { + const commonValues: any = {}; + fields.forEach(field => { + const values = duplicateGroup.map(doc => doc[field]); + const uniqueValues = [...new Set(values)]; + if (uniqueValues.length === 1) { + commonValues[field] = uniqueValues[0]; + } + }); + + groups.push({ + documents: duplicateGroup, + similarity: this.calculateGroupSimilarity(duplicateGroup, fields), + commonValues + }); + + totalDuplicates += duplicateGroup.length; + } + } + + return { + groups, + totalDuplicates, + exactMatches, + similarMatches + }; + } + + private calculateSimilarity(doc1: any, doc2: any, fields: string[]): number { + let matches = 0; + let total = 0; + + fields.forEach(field => { + total++; + const val1 = String(doc1[field] || '').toLowerCase().trim(); + const val2 = String(doc2[field] || '').toLowerCase().trim(); + + if (val1 === val2) { + matches++; + } else if (val1 && val2) { + // Fuzzy matching for strings + const similarity = this.stringSimilarity(val1, val2); + matches += similarity; + } + }); + + return total > 0 ? matches / total : 0; + } + + private stringSimilarity(str1: string, str2: string): number { + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + + if (longer.length === 0) return 1.0; + + const editDistance = this.levenshteinDistance(longer, shorter); + return (longer.length - editDistance) / longer.length; + } + + private levenshteinDistance(str1: string, str2: string): number { + const matrix = []; + + for (let i = 0; i <= str2.length; i++) { + matrix[i] = [i]; + } + + for (let j = 0; j <= str1.length; j++) { + matrix[0][j] = j; + } + + for (let i = 1; i <= str2.length; i++) { + for (let j = 1; j <= str1.length; j++) { + if (str2.charAt(i - 1) === str1.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j] + 1 + ); + } + } + } + + return matrix[str2.length][str1.length]; + } + + private calculateGroupSimilarity(documents: any[], fields: string[]): number { + let totalSimilarity = 0; + let comparisons = 0; + + for (let i = 0; i < documents.length; i++) { + for (let j = i + 1; j < documents.length; j++) { + totalSimilarity += this.calculateSimilarity(documents[i], documents[j], fields); + comparisons++; + } + } + + return comparisons > 0 ? totalSimilarity / comparisons : 0; + } + + private async dataQualityCheck(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, checkType = 'all' } = args; + + try { + const attributes = await this.databases.listAttributes(databaseId, collectionId); + const documents = await this.databases.listDocuments( + databaseId, + collectionId, + [Query.limit(2000)] + ); + + const qualityReport = this.performDataQualityCheck(documents.documents, attributes.attributes, checkType); + + return { + content: [ + { + type: "text", + text: `Data Quality Report: ${collectionId} + +๐Ÿ“Š Overall Quality Score: ${qualityReport.overallScore}/100 + +${checkType === 'all' || checkType === 'completeness' ? ` +๐Ÿ” Completeness Check: +${qualityReport.completeness.map((item: any) => + `โ€ข ${item.field}: ${item.completeness}% complete (${item.missingCount} missing)` +).join('\n')}` : ''} + +${checkType === 'all' || checkType === 'validity' ? ` +โœ… Validity Check: +${qualityReport.validity.map((item: any) => + `โ€ข ${item.field}: ${item.validCount}/${item.totalCount} valid (${item.errorTypes.join(', ') || 'No errors'})` +).join('\n')}` : ''} + +${checkType === 'all' || checkType === 'consistency' ? ` +๐Ÿ”„ Consistency Check: +${qualityReport.consistency.map((item: any) => + `โ€ข ${item.field}: ${item.consistencyScore}% consistent (${item.issues.join(', ') || 'No issues'})` +).join('\n')}` : ''} + +๐Ÿšจ Issues Found (${qualityReport.totalIssues}): +${qualityReport.issues.slice(0, 10).map((issue: any) => + `โ€ข ${issue.severity.toUpperCase()}: ${issue.field} - ${issue.description}` +).join('\n')} + +๐Ÿ’ก Recommendations: +${qualityReport.recommendations.map((rec: string) => `โ€ข ${rec}`).join('\n')} + +๐Ÿ“ˆ Quality Trends: +โ€ข Fields with >90% quality: ${qualityReport.highQualityFields} +โ€ข Fields needing attention: ${qualityReport.lowQualityFields.length} +โ€ข Data integrity score: ${qualityReport.integrityScore}/100` + } + ] + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Data Quality Check Failed: + +Error: ${error} + +Please ensure: +โ€ข Collection exists and is accessible +โ€ข You have read permissions +โ€ข Collection contains data to analyze` + } + ] + }; + } + } + + private performDataQualityCheck(documents: any[], attributes: any[], checkType: string): any { + const report: any = { + completeness: [], + validity: [], + consistency: [], + issues: [], + recommendations: [], + totalIssues: 0, + overallScore: 0, + highQualityFields: 0, + lowQualityFields: [], + integrityScore: 0 + }; + + // Completeness check + if (checkType === 'all' || checkType === 'completeness') { + attributes.forEach(attr => { + const missingCount = documents.filter(doc => + doc[attr.key] === null || + doc[attr.key] === undefined || + doc[attr.key] === '' + ).length; + + const completeness = ((documents.length - missingCount) / documents.length) * 100; + + report.completeness.push({ + field: attr.key, + completeness: completeness.toFixed(1), + missingCount, + totalCount: documents.length + }); + + if (missingCount > 0 && attr.required) { + report.issues.push({ + severity: 'high', + field: attr.key, + description: `Required field has ${missingCount} missing values` + }); + } + }); + } + + // Validity check + if (checkType === 'all' || checkType === 'validity') { + attributes.forEach(attr => { + let validCount = 0; + const errorTypes: string[] = []; + + documents.forEach(doc => { + const value = doc[attr.key]; + if (value === null || value === undefined) return; + + let isValid = true; + + switch (attr.type) { + case 'email': + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) { + isValid = false; + if (!errorTypes.includes('invalid_email')) errorTypes.push('invalid_email'); + } + break; + case 'integer': + if (!Number.isInteger(value)) { + isValid = false; + if (!errorTypes.includes('not_integer')) errorTypes.push('not_integer'); + } + break; + case 'boolean': + if (typeof value !== 'boolean') { + isValid = false; + if (!errorTypes.includes('not_boolean')) errorTypes.push('not_boolean'); + } + break; + case 'datetime': + if (isNaN(Date.parse(value))) { + isValid = false; + if (!errorTypes.includes('invalid_date')) errorTypes.push('invalid_date'); + } + break; + case 'string': + if (attr.size && String(value).length > attr.size) { + isValid = false; + if (!errorTypes.includes('exceeds_length')) errorTypes.push('exceeds_length'); + } + break; + } + + if (isValid) validCount++; + }); + + report.validity.push({ + field: attr.key, + validCount, + totalCount: documents.length, + errorTypes + }); + + if (errorTypes.length > 0) { + report.issues.push({ + severity: 'medium', + field: attr.key, + description: `Validation errors: ${errorTypes.join(', ')}` + }); + } + }); + } + + // Consistency check + if (checkType === 'all' || checkType === 'consistency') { + attributes.forEach(attr => { + const values = documents.map(doc => doc[attr.key]).filter(val => val !== null && val !== undefined); + const issues = []; + let consistencyScore = 100; + + if (attr.type === 'string') { + // Check for case inconsistencies + const caseVariations = new Set(values.map(val => String(val).toLowerCase())); + const originalVariations = new Set(values.map(val => String(val))); + + if (originalVariations.size > caseVariations.size) { + issues.push('case_inconsistency'); + consistencyScore -= 20; + } + + // Check for whitespace inconsistencies + const trimmedValues = values.map(val => String(val).trim()); + if (trimmedValues.some((val, idx) => val !== values[idx])) { + issues.push('whitespace_issues'); + consistencyScore -= 15; + } + } + + report.consistency.push({ + field: attr.key, + consistencyScore: Math.max(0, consistencyScore), + issues + }); + + if (issues.length > 0) { + report.issues.push({ + severity: 'low', + field: attr.key, + description: `Consistency issues: ${issues.join(', ')}` + }); + } + }); + } + + // Calculate scores + report.totalIssues = report.issues.length; + + const completenessScores = report.completeness.map((c: any) => parseFloat(c.completeness)); + const validityScores = report.validity.map((v: any) => (v.validCount / v.totalCount) * 100); + const consistencyScores = report.consistency.map((c: any) => c.consistencyScore); + + const allScores = [...completenessScores, ...validityScores, ...consistencyScores]; + report.overallScore = allScores.length > 0 ? + Math.round(allScores.reduce((sum, score) => sum + score, 0) / allScores.length) : 0; + + report.highQualityFields = allScores.filter(score => score > 90).length; + report.lowQualityFields = attributes.filter((attr, idx) => + (completenessScores[idx] || 0) < 70 || + (validityScores[idx] || 0) < 70 || + (consistencyScores[idx] || 0) < 70 + ); + + report.integrityScore = Math.max(0, 100 - (report.totalIssues * 5)); + + // Generate recommendations + if (report.lowQualityFields.length > 0) { + report.recommendations.push(`Focus on improving ${report.lowQualityFields.length} low-quality fields`); + } + if (report.totalIssues > 10) { + report.recommendations.push('Consider implementing data validation at input level'); + } + if (report.overallScore < 80) { + report.recommendations.push('Overall data quality needs improvement - consider data cleansing'); + } + + return report; + } + + private async usageStats(args: any) { + if (!this.databases) throw new Error("Databases not initialized"); + + const { databaseId, collectionId, timeRange = '7d' } = args; + + try { + // Note: Appwrite doesn't provide usage analytics, so we'll simulate based on available data + const stats = await this.generateUsageStats(databaseId, collectionId, timeRange); + + return { + content: [ + { + type: "text", + text: `Usage Statistics Report: + +๐Ÿ“Š Overview (Last ${timeRange}): +โ€ข Database: ${databaseId} +${collectionId ? `โ€ข Collection: ${collectionId}` : 'โ€ข All Collections'} + +๐Ÿ“ˆ Document Activity: +${JSON.stringify(stats.documentActivity, null, 2)} + +๐Ÿ” Query Patterns (Estimated): +${JSON.stringify(stats.queryPatterns, null, 2)} + +๐Ÿ“ Collection Usage: +${JSON.stringify(stats.collectionUsage, null, 2)} + +โšก Performance Insights: +${JSON.stringify(stats.performance, null, 2)} + +๐Ÿ’ก Optimization Suggestions: +${stats.suggestions.map((suggestion: string) => `โ€ข ${suggestion}`).join('\n')} + +โš ๏ธ Note: Usage statistics are estimated based on available data. +For accurate analytics, consider implementing custom tracking or using Appwrite's built-in analytics when available.` + } + ] + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Usage Statistics Failed: + +Error: ${error} + +Note: Appwrite doesn't provide detailed usage analytics through the API. +This tool provides estimated statistics based on available document data. + +For accurate usage tracking, consider: +โ€ข Implementing custom analytics in your application +โ€ข Using Appwrite Console analytics (when available) +โ€ข Adding logging to your application queries` + } + ] + }; + } + } + + private async generateUsageStats(databaseId: string, collectionId?: string, timeRange: string = '7d'): Promise { + const stats: any = { + documentActivity: {}, + queryPatterns: {}, + collectionUsage: {}, + performance: {}, + suggestions: [] + }; + + // Get time range + const days = parseInt(timeRange.replace('d', '')); + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + if (collectionId) { + // Single collection stats + const documents = await this.databases!.listDocuments(databaseId, collectionId, [Query.limit(1000)]); + const attributes = await this.databases!.listAttributes(databaseId, collectionId); + const indexes = await this.databases!.listIndexes(databaseId, collectionId); + + // Document activity (based on creation dates) + const recentDocs = documents.documents.filter(doc => + new Date(doc.$createdAt) >= startDate + ); + + stats.documentActivity = { + totalDocuments: documents.total, + recentDocuments: recentDocs.length, + creationRate: (recentDocs.length / days).toFixed(1) + ' docs/day', + lastActivity: documents.documents.length > 0 ? + documents.documents[0].$updatedAt : 'N/A' + }; + + // Estimated query patterns + stats.queryPatterns = { + estimatedReads: Math.floor(documents.total * 0.1 * days), // Estimate + estimatedWrites: recentDocs.length, + popularFields: attributes.attributes + .filter((attr: any) => attr.key.match(/(id|name|status|email)/i)) + .map((attr: any) => attr.key), + indexUtilization: (indexes.total / Math.max(1, attributes.total) * 100).toFixed(1) + '%' + }; + + // Performance insights + stats.performance = { + attributeCount: attributes.total, + indexCount: indexes.total, + avgDocumentSize: this.estimateDocumentSize(documents.documents), + recommendedIndexes: indexes.total < 3 ? 'Consider adding more indexes' : 'Good index coverage' + }; + + } else { + // All collections stats + const databases = await this.databases!.list(); + const collections = await this.databases!.listCollections(databaseId); + + stats.collectionUsage = {}; + let totalDocs = 0; + + for (const collection of collections.collections.slice(0, 5)) { // Limit for performance + try { + const docs = await this.databases!.listDocuments(databaseId, collection.$id, [Query.limit(100)]); + const recentDocs = docs.documents.filter(doc => + new Date(doc.$createdAt) >= startDate + ); + + stats.collectionUsage[collection.name] = { + totalDocuments: docs.total, + recentActivity: recentDocs.length, + lastUpdated: docs.documents.length > 0 ? docs.documents[0].$updatedAt : 'N/A' + }; + + totalDocs += docs.total; + } catch (error) { + // Skip collections we can't access + } + } + + stats.documentActivity = { + totalCollections: collections.total, + totalDocuments: totalDocs, + averageDocsPerCollection: (totalDocs / collections.total).toFixed(1), + timeRange: timeRange + }; + } + + // Generate suggestions + if (collectionId) { + const docs = await this.databases!.listDocuments(databaseId, collectionId, [Query.limit(100)]); + const indexes = await this.databases!.listIndexes(databaseId, collectionId); + + if (indexes.total === 0 && docs.total > 100) { + stats.suggestions.push('Add indexes to improve query performance on large collection'); + } + if (docs.total > 10000) { + stats.suggestions.push('Consider pagination for large result sets'); + } + if (stats.queryPatterns?.indexUtilization && parseFloat(stats.queryPatterns.indexUtilization) < 50) { + stats.suggestions.push('Low index utilization - review query patterns and add targeted indexes'); + } + } + + return stats; + } + + private estimateDocumentSize(documents: any[]): string { + if (documents.length === 0) return 'N/A'; + + const sample = documents.slice(0, 10); + const totalSize = sample.reduce((sum, doc) => { + return sum + JSON.stringify(doc).length; + }, 0); + + const avgSize = totalSize / sample.length; + return avgSize < 1024 ? + Math.round(avgSize) + ' bytes' : + (avgSize / 1024).toFixed(1) + ' KB'; + } + + async run(): Promise { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error("Appwrite MCP server running on stdio"); + } +} + +const server = new AppwriteMCPServer(); +server.run().catch(console.error); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d51cffc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file