Compare commits

...

4 Commits

2 changed files with 335 additions and 293 deletions

View File

@@ -6,33 +6,32 @@
![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
![Claude](https://img.shields.io/badge/Claude-AI-orange?style=for-the-badge) ![Claude](https://img.shields.io/badge/Claude-AI-orange?style=for-the-badge)
**Transform Claude into your intelligent Appwrite management assistant** **Transform Claude into your intelligent Appwrite management assistant**
*16 optimized tools • Core Appwrite operations • Context-efficient design • Production-ready* *12 optimized tools • Core Appwrite operations • Context-efficient design • Production-ready*
</div> </div>
--- ---
A **context-optimized Model Context Protocol (MCP)** server that provides Claude with essential Appwrite integration. This version focuses on core functionality with action-based combined tools, offering database operations, user management, storage, messaging, and team management in an efficient package designed to stay under MCP context limits. A **context-optimized Model Context Protocol (MCP)** server that provides Claude with essential Appwrite integration. This version focuses on core functionality with action-based combined tools, offering database operations, user management, storage, and team management in an efficient package designed to stay under MCP context limits.
## 🚀 Features ## 🚀 Features
**🎯 Core Appwrite Operations:** **🎯 Core Appwrite Operations:**
- **Database Management** - Create, list, and delete databases - **Database Management** - Create, get, list, update, and delete databases with enabled status
- **Collection Operations** - Full collection lifecycle management - **Collection Operations** - Full collection lifecycle with document security and search support
- **Document CRUD** - Complete document operations with bulk support - **Document CRUD** - Complete document operations with bulk support and advanced queries
- **User Management** - User operations including bulk processing and preferences - **User Management** - User operations including bulk processing and preferences
- **Storage Management** - Bucket and file operations with URL generation - **Storage Management** - Complete bucket operations with advanced configuration (file size limits, allowed extensions, compression, encryption, antivirus) and file upload/download
- **Team Management** - Complete team administration - **Team Management** - Team CRUD operations
- **Messaging Service** - Messages, topics, and subscriber management - **Attribute & Index Management** - Schema management with array support and bulk operations
- **Attribute & Index Management** - Schema management with bulk operations
- **Health Monitoring** - System health checks - **Health Monitoring** - System health checks
**⚡ Action-Based Design:** **⚡ Action-Based Design:**
- **Single tools handle multiple operations** using action parameters - **Single tools handle multiple operations** using action parameters
- **Bulk operations integrated** into respective categories - **Bulk operations integrated** into respective categories
- **Context-optimized** - 88% fewer tools than original - **Context-optimized** - Efficient tool design for reduced token usage
- **Maintained functionality** - All core features preserved - **Advanced features** - Document security, file download, bucket encryption, array attributes
## 📦 Quick Start ## 📦 Quick Start
@@ -167,14 +166,14 @@ Once integrated, you should be able to use commands like:
- "Create a new user with email test@example.com" - "Create a new user with email test@example.com"
- "Show me all collections in my database" - "Show me all collections in my database"
## 🛠️ Available Tools (16 Total) ## 🛠️ Available Tools (12 Total)
<details> <details>
<summary><strong>🗄️ Database Operations (1 tool)</strong></summary> <summary><strong>🗄️ Database Operations (1 tool)</strong></summary>
| Tool | Actions | Description | | Tool | Actions | Description |
|------|---------|-------------| |------|---------|-------------|
| `manage_database` | create, list, delete | Comprehensive database management | | `manage_database` | create, get, list, update, delete | Comprehensive database management with enabled status |
</details> </details>
@@ -183,7 +182,7 @@ Once integrated, you should be able to use commands like:
| Tool | Actions | Description | | Tool | Actions | Description |
|------|---------|-------------| |------|---------|-------------|
| `collection_operations` | create, get, list, update, delete | Complete collection lifecycle management | | `collection_operations` | create, get, list, update, delete | Complete collection lifecycle with document security, enabled status, and search |
</details> </details>
@@ -192,7 +191,7 @@ Once integrated, you should be able to use commands like:
| Tool | Actions | Description | | Tool | Actions | Description |
|------|---------|-------------| |------|---------|-------------|
| `attribute_operations` | create, get, list, update, delete, bulk_create, bulk_delete | Full attribute management with bulk operations | | `attribute_operations` | create, get, list, update, delete, bulk_create, bulk_delete | Full attribute management with array support and bulk operations |
</details> </details>
@@ -230,8 +229,8 @@ Once integrated, you should be able to use commands like:
| Tool | Actions/Description | | Tool | Actions/Description |
|------|--------------------| |------|--------------------|
| `bucket_operations` | create, get, list, update, delete | | `bucket_operations` | create, get, list, update, delete - with advanced configuration (file size limits, allowed extensions, compression, encryption, antivirus) |
| `file_operations` | get, create, update, delete, list | | `file_operations` | get, create, update, delete, list, download - with actual file download implementation |
| `get_file_url` | Generate download, preview, or view URLs with transformations | | `get_file_url` | Generate download, preview, or view URLs with transformations |
</details> </details>
@@ -246,17 +245,6 @@ Once integrated, you should be able to use commands like:
</details> </details>
<details>
<summary><strong>📧 Messaging Operations (3 tools)</strong></summary>
| Tool | Actions | Description |
|------|---------|-------------|
| `messaging_message_operations` | create, get, list, update, delete | Complete message management |
| `messaging_topic_operations` | create, get, list, update, delete | Complete topic management |
| `messaging_subscriber_operations` | create, get, list, delete | Complete subscriber management |
</details>
<details> <details>
<summary><strong>🏥 Health Monitoring (1 tool)</strong></summary> <summary><strong>🏥 Health Monitoring (1 tool)</strong></summary>
@@ -278,21 +266,21 @@ Once integrated, you should be able to use commands like:
- **Appwrite Cloud**: `https://cloud.appwrite.io/v1` - **Appwrite Cloud**: `https://cloud.appwrite.io/v1`
- **Self-hosted**: `https://your-domain.com/v1` - **Self-hosted**: `https://your-domain.com/v1`
3. **🔐 API Key**: Create a new API key in your Appwrite console under **Settings > API Keys** 3. **🔐 API Key**: Create a new API key in your Appwrite console under **Settings > API Keys**
- ✅ Required scopes: `databases.read`, `databases.write`, `users.read`, `users.write`, `storage.read`, `storage.write`, `messages.read`, `messages.write`, `topics.read`, `topics.write`, `subscribers.read`, `subscribers.write`, `teams.read`, `teams.write`, `health.read` - ✅ Required scopes: `databases.read`, `databases.write`, `users.read`, `users.write`, `storage.read`, `storage.write`, `teams.read`, `teams.write`, `health.read`
### 🎯 VS Official Implementation ### 🎯 VS Official Implementation
| Feature | Our Implementation | Official Appwrite MCP | | Feature | Our Implementation | Official Appwrite MCP |
|---------|-------------------|----------------------| |---------|-------------------|----------------------|
| **Tools Available** | 🟢 16 optimized tools (context-efficient) | 🟡 195 tools (selective enabling) | | **Tools Available** | 🟢 12 optimized tools (context-efficient) | 🟡 195 tools (selective enabling) |
| **Context Usage** | 🟢 Under 25,000 tokens (88% reduction) | 🔴 Over 60,000 tokens | | **Context Usage** | 🟢 Optimized token usage | 🔴 Over 60,000 tokens |
| **Tool Design** | 🟢 Action-based combined tools | 🟡 Individual tools for each operation | | **Tool Design** | 🟢 Action-based combined tools | 🟡 Individual tools for each operation |
| **Core Operations** | 🟢 All essential Appwrite features | 🟡 Database tools only (context limits) | | **Core Operations** | 🟢 All essential database, user, storage, and team features | 🟡 Database tools only (context limits) |
| **Messaging Service** | 🟢 Messages, topics, subscribers | 🟡 Selective enabling required | | **Storage Operations** | 🟢 Complete file operations with download & advanced bucket config | 🟡 Basic bucket operations |
| **Storage Operations** | 🟢 Complete file operations & uploads | 🟡 Basic bucket operations |
| **User Management** | 🟢 CRUD + preferences + bulk operations | 🟡 Basic CRUD only | | **User Management** | 🟢 CRUD + preferences + bulk operations | 🟡 Basic CRUD only |
| **Advanced Features** | 🟢 Document security, file encryption, array attributes, compression | 🔴 Limited |
| **Bulk Operations** | 🟢 Integrated into respective categories | 🔴 Not available | | **Bulk Operations** | 🟢 Integrated into respective categories | 🔴 Not available |
| **Error Handling** | 🟢 Comprehensive | 🟡 Basic | | **Error Handling** | 🟢 Comprehensive with validation | 🟡 Basic |
| **Language** | 🟡 TypeScript/Node.js | 🟢 Python | | **Language** | 🟡 TypeScript/Node.js | 🟢 Python |
| **Maintenance** | 🟢 Easier with fewer tools | 🟡 Complex with many tools | | **Maintenance** | 🟢 Easier with fewer tools | 🟡 Complex with many tools |
@@ -372,18 +360,6 @@ Once integrated, you can use natural language commands with Claude to interact w
<details>
<summary><strong>📧 Messaging & Communication</strong></summary>
```
✨ "List all messaging topics"
✨ "Create a topic for user announcements"
✨ "Add subscribers to the announcements topic"
✨ "Send a message to all subscribers"
✨ "List all messages and their delivery status"
```
</details>

View File

@@ -8,23 +8,22 @@ import {
ListToolsRequestSchema, ListToolsRequestSchema,
McpError, McpError,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { import {
Client, Client,
Databases, Databases,
Users, Users,
Storage, Storage,
Teams, Teams,
Account, Account,
Health, Health,
Messaging, ID,
ID, Query,
Query, Permission,
Permission,
Role, Role,
IndexType IndexType
} from "node-appwrite"; } from "node-appwrite";
import { InputFile } from "node-appwrite/file"; import { InputFile } from "node-appwrite/file";
import { readFileSync, existsSync } from "fs"; import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path"; import { join } from "path";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
@@ -43,7 +42,6 @@ class AppwriteMCPServer {
private teams: Teams | null = null; private teams: Teams | null = null;
private account: Account | null = null; private account: Account | null = null;
private health: Health | null = null; private health: Health | null = null;
private messaging: Messaging | null = null;
private config: AppwriteConfig | null = null; private config: AppwriteConfig | null = null;
constructor() { constructor() {
@@ -99,7 +97,6 @@ class AppwriteMCPServer {
this.teams = new Teams(this.client); this.teams = new Teams(this.client);
this.account = new Account(this.client); this.account = new Account(this.client);
this.health = new Health(this.client); this.health = new Health(this.client);
this.messaging = new Messaging(this.client);
} }
private setupToolHandlers(): void { private setupToolHandlers(): void {
@@ -109,13 +106,14 @@ class AppwriteMCPServer {
// Database Operations // Database Operations
{ {
name: "manage_database", name: "manage_database",
description: "Manage database operations (create, list, delete)", description: "Manage database operations (create, get, list, update, delete)",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
action: { type: "string", description: "Action to perform", enum: ["create", "list", "delete"] }, action: { type: "string", description: "Action to perform", enum: ["create", "get", "list", "update", "delete"] },
databaseId: { type: "string", description: "ID of the database (required for delete, optional for create)" }, databaseId: { type: "string", description: "ID of the database (required for get/update/delete, optional for create)" },
name: { type: "string", description: "Name of the database (required for create)" } name: { type: "string", description: "Name of the database (required for create/update)" },
enabled: { type: "boolean", description: "Enable or disable database (optional for create/update)" }
}, },
required: ["action"] required: ["action"]
} }
@@ -132,7 +130,11 @@ class AppwriteMCPServer {
databaseId: { type: "string", description: "ID of the database" }, databaseId: { type: "string", description: "ID of the database" },
collectionId: { type: "string", description: "ID of the collection (optional for create/list, required for get/update/delete)" }, collectionId: { type: "string", description: "ID of the collection (optional for create/list, required for get/update/delete)" },
name: { type: "string", description: "Collection name (required for create/update)" }, name: { type: "string", description: "Collection name (required for create/update)" },
permissions: { type: "array", description: "Collection permissions (optional)", items: { type: "string" } } permissions: { type: "array", description: "Collection permissions (optional)", items: { type: "string" } },
documentSecurity: { type: "boolean", description: "Enable document-level security (optional)" },
enabled: { type: "boolean", description: "Enable or disable collection (optional)" },
queries: { type: "array", description: "Query filters for list action (optional)", items: { type: "string" } },
search: { type: "string", description: "Search term for filtering results (optional)" }
}, },
required: ["action", "databaseId"] required: ["action", "databaseId"]
} }
@@ -149,13 +151,14 @@ class AppwriteMCPServer {
databaseId: { type: "string", description: "ID of the database" }, databaseId: { type: "string", description: "ID of the database" },
collectionId: { type: "string", description: "ID of the collection" }, collectionId: { type: "string", description: "ID of the collection" },
key: { type: "string", description: "Attribute key (required for create/get/update/delete)" }, key: { type: "string", description: "Attribute key (required for create/get/update/delete)" },
type: { type: {
type: "string", type: "string",
description: "Attribute type (required for create/update)", description: "Attribute type (required for create/update)",
enum: ["string", "integer", "float", "boolean", "datetime", "email", "ip", "url", "enum", "relationship"] enum: ["string", "integer", "float", "boolean", "datetime", "email", "ip", "url", "enum", "relationship"]
}, },
required: { type: "boolean", description: "Is attribute required (required for create/update)" }, required: { type: "boolean", description: "Is attribute required (required for create/update)" },
default: { type: "string", description: "Default value (optional)" }, default: { type: "string", description: "Default value (optional)" },
array: { type: "boolean", description: "Is attribute an array (optional, default: false)" },
size: { type: "number", description: "Size for string/ip/url attributes (optional)" }, size: { type: "number", description: "Size for string/ip/url attributes (optional)" },
min: { type: "number", description: "Minimum value for integer/float attributes (optional)" }, min: { type: "number", description: "Minimum value for integer/float attributes (optional)" },
max: { type: "number", description: "Maximum value for integer/float attributes (optional)" }, max: { type: "number", description: "Maximum value for integer/float attributes (optional)" },
@@ -179,13 +182,14 @@ class AppwriteMCPServer {
type: "object", type: "object",
properties: { properties: {
key: { type: "string", description: "Attribute key" }, key: { type: "string", description: "Attribute key" },
type: { type: {
type: "string", type: "string",
description: "Attribute type", description: "Attribute type",
enum: ["string", "integer", "float", "boolean", "datetime", "email", "ip", "url", "enum", "relationship"] enum: ["string", "integer", "float", "boolean", "datetime", "email", "ip", "url", "enum", "relationship"]
}, },
required: { type: "boolean", description: "Is attribute required" }, required: { type: "boolean", description: "Is attribute required" },
default: { type: "string", description: "Default value (optional)" }, default: { type: "string", description: "Default value (optional)" },
array: { type: "boolean", description: "Is attribute an array (optional, default: false)" },
size: { type: "number", description: "Size for string/ip/url attributes (optional)" }, size: { type: "number", description: "Size for string/ip/url attributes (optional)" },
min: { type: "number", description: "Minimum value for integer/float attributes (optional)" }, min: { type: "number", description: "Minimum value for integer/float attributes (optional)" },
max: { type: "number", description: "Maximum value for integer/float attributes (optional)" }, max: { type: "number", description: "Maximum value for integer/float attributes (optional)" },
@@ -380,7 +384,12 @@ class AppwriteMCPServer {
name: { type: "string", description: "Bucket name (required for create/update)" }, name: { type: "string", description: "Bucket name (required for create/update)" },
permissions: { type: "array", description: "Bucket permissions (optional)", items: { type: "string" } }, permissions: { type: "array", description: "Bucket permissions (optional)", items: { type: "string" } },
fileSecurity: { type: "boolean", description: "Enable file security (optional)" }, fileSecurity: { type: "boolean", description: "Enable file security (optional)" },
enabled: { type: "boolean", description: "Enable bucket (optional)" } enabled: { type: "boolean", description: "Enable bucket (optional)" },
maximumFileSize: { type: "number", description: "Maximum file size in bytes (optional, max 50MB)" },
allowedFileExtensions: { type: "array", description: "Allowed file extensions (optional)", items: { type: "string" } },
compression: { type: "string", description: "Compression algorithm (optional)", enum: ["none", "gzip", "zstd"] },
encryption: { type: "boolean", description: "Enable encryption (optional)" },
antivirus: { type: "boolean", description: "Enable antivirus scanning (optional)" }
}, },
required: ["action"] required: ["action"]
} }
@@ -389,14 +398,15 @@ class AppwriteMCPServer {
// File Operations // File Operations
{ {
name: "file_operations", name: "file_operations",
description: "Manage file operations (get, create, update, delete, list)", description: "Manage file operations (get, create, update, delete, list, download)",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
action: { type: "string", description: "Action to perform", enum: ["get", "create", "update", "delete", "list"] }, action: { type: "string", description: "Action to perform", enum: ["get", "create", "update", "delete", "list", "download"] },
bucketId: { type: "string", description: "Bucket ID" }, bucketId: { type: "string", description: "Bucket ID" },
fileId: { type: "string", description: "File ID (optional for create/list, required for get/update/delete)" }, fileId: { type: "string", description: "File ID (optional for create/list, required for get/update/delete/download)" },
filePath: { type: "string", description: "Local file path to upload (required for create)" }, filePath: { type: "string", description: "Local file path to upload (required for create)" },
downloadPath: { type: "string", description: "Local path to save downloaded file (required for download)" },
name: { type: "string", description: "File name (optional for update)" }, name: { type: "string", description: "File name (optional for update)" },
permissions: { type: "array", description: "File permissions (optional)", items: { type: "string" } } permissions: { type: "array", description: "File permissions (optional)", items: { type: "string" } }
}, },
@@ -446,62 +456,7 @@ class AppwriteMCPServer {
} }
}, },
// Messaging Message Operations // Health Monitoring
{
name: "messaging_message_operations",
description: "Manage messaging message operations (create, get, list, update, delete)",
inputSchema: {
type: "object",
properties: {
action: { type: "string", description: "Action to perform", enum: ["create", "get", "list", "update", "delete"] },
messageId: { type: "string", description: "Message ID (optional for create/list, required for get/update/delete)" },
subject: { type: "string", description: "Message subject (optional)" },
content: { type: "string", description: "Message content (required for create)" },
topics: { type: "array", description: "Topic IDs (optional)", items: { type: "string" } },
users: { type: "array", description: "User IDs (optional)", items: { type: "string" } },
targets: { type: "array", description: "Target IDs (optional)", items: { type: "string" } },
status: { type: "string", description: "Message status (optional for update)" },
queries: { type: "array", description: "Query filters (optional for list)", items: { type: "string" } }
},
required: ["action"]
}
},
// Messaging Topic Operations
{
name: "messaging_topic_operations",
description: "Manage messaging topic operations (create, get, list, update, delete)",
inputSchema: {
type: "object",
properties: {
action: { type: "string", description: "Action to perform", enum: ["create", "get", "list", "update", "delete"] },
topicId: { type: "string", description: "Topic ID (optional for create/list, required for get/update/delete)" },
name: { type: "string", description: "Topic name (required for create)" },
description: { type: "string", description: "Topic description (optional)" },
queries: { type: "array", description: "Query filters (optional for list)", items: { type: "string" } }
},
required: ["action"]
}
},
// Messaging Subscriber Operations
{
name: "messaging_subscriber_operations",
description: "Manage messaging subscriber operations (create, get, list, delete)",
inputSchema: {
type: "object",
properties: {
action: { type: "string", description: "Action to perform", enum: ["create", "get", "list", "delete"] },
topicId: { type: "string", description: "Topic ID" },
subscriberId: { type: "string", description: "Subscriber ID (optional for create/list, required for get/delete)" },
targetId: { type: "string", description: "Target ID (required for create)" },
queries: { type: "array", description: "Query filters (optional for list)", items: { type: "string" } }
},
required: ["action", "topicId"]
}
},
// Health Monitoring (kept separate)
{ {
name: "get_health", name: "get_health",
description: "Get overall health status of Appwrite services", description: "Get overall health status of Appwrite services",
@@ -568,16 +523,6 @@ class AppwriteMCPServer {
case "team_operations": case "team_operations":
return await this.teamOperations(request.params.arguments); return await this.teamOperations(request.params.arguments);
// Messaging Operations
case "messaging_message_operations":
return await this.messagingMessageOperations(request.params.arguments);
case "messaging_topic_operations":
return await this.messagingTopicOperations(request.params.arguments);
case "messaging_subscriber_operations":
return await this.messagingSubscriberOperations(request.params.arguments);
case "get_health": case "get_health":
return await this.getHealth(); return await this.getHealth();
@@ -600,25 +545,40 @@ class AppwriteMCPServer {
private async manageDatabaseOperations(args: any) { private async manageDatabaseOperations(args: any) {
if (!this.databases) throw new Error("Databases not initialized"); if (!this.databases) throw new Error("Databases not initialized");
const { action, databaseId, name } = args; const { action, databaseId, name, enabled } = args;
switch (action) { switch (action) {
case "create": case "create":
if (!name) throw new Error("Name is required for create action"); if (!name) throw new Error("Name is required for create action");
const dbId = databaseId || ID.unique(); const dbId = databaseId || ID.unique();
const database = await this.databases.create(dbId, name); const database = await this.databases.create(dbId, name, enabled);
return { return {
content: [{ type: "text", text: `Database created successfully:\n- ID: ${database.$id}\n- Name: ${database.name}\n- Created: ${database.$createdAt}` }] content: [{ type: "text", text: `Database created successfully:\n- ID: ${database.$id}\n- Name: ${database.name}\n- Enabled: ${database.enabled}\n- Created: ${database.$createdAt}` }]
};
case "get":
if (!databaseId) throw new Error("databaseId is required for get action");
const getDb = await this.databases.get(databaseId);
return {
content: [{ type: "text", text: `Database Details:\n- ID: ${getDb.$id}\n- Name: ${getDb.name}\n- Enabled: ${getDb.enabled}\n- Created: ${getDb.$createdAt}\n- Updated: ${getDb.$updatedAt}` }]
}; };
case "list": case "list":
const databases = await this.databases.list(); const databases = await this.databases.list();
const dbList = databases.databases.map(db => `- ${db.name} (${db.$id})`).join('\n'); const dbList = databases.databases.map(db => `- ${db.name} (${db.$id}) - ${db.enabled ? 'enabled' : 'disabled'}`).join('\n');
return { return {
content: [{ type: "text", text: `Databases (${databases.total}):\n${dbList}` }] content: [{ type: "text", text: `Databases (${databases.total}):\n${dbList}` }]
}; };
case "update":
if (!databaseId) throw new Error("databaseId is required for update action");
if (!name) throw new Error("name is required for update action");
const updatedDb = await this.databases.update(databaseId, name, enabled);
return {
content: [{ type: "text", text: `Database updated successfully:\n- ID: ${updatedDb.$id}\n- Name: ${updatedDb.name}\n- Enabled: ${updatedDb.enabled}` }]
};
case "delete": case "delete":
if (!databaseId) throw new Error("databaseId is required for delete action"); if (!databaseId) throw new Error("databaseId is required for delete action");
await this.databases.delete(databaseId); await this.databases.delete(databaseId);
@@ -633,28 +593,38 @@ class AppwriteMCPServer {
private async collectionOperations(args: any) { private async collectionOperations(args: any) {
if (!this.databases) throw new Error("Databases not initialized"); if (!this.databases) throw new Error("Databases not initialized");
const { action, databaseId, collectionId, name, permissions } = args; const { action, databaseId, collectionId, name, permissions, documentSecurity, enabled, queries, search } = args;
switch (action) { switch (action) {
case "create": case "create":
if (!name) throw new Error("Name is required for create action"); if (!name) throw new Error("Name is required for create action");
const colId = collectionId || ID.unique(); const colId = collectionId || ID.unique();
const collection = await this.databases.createCollection(databaseId, colId, name, permissions); const collection = await this.databases.createCollection(databaseId, colId, name, permissions, documentSecurity, enabled);
return { return {
content: [{ type: "text", text: `Collection created successfully:\n- ID: ${collection.$id}\n- Name: ${collection.name}\n- Database: ${collection.databaseId}` }] content: [{ type: "text", text: `Collection created successfully:\n- ID: ${collection.$id}\n- Name: ${collection.name}\n- Database: ${collection.databaseId}\n- Document Security: ${collection.documentSecurity}\n- Enabled: ${collection.enabled}` }]
}; };
case "get": case "get":
if (!collectionId) throw new Error("collectionId is required for get action"); if (!collectionId) throw new Error("collectionId is required for get action");
const getCollection = await this.databases.getCollection(databaseId, collectionId); const getCollection = await this.databases.getCollection(databaseId, collectionId);
return { return {
content: [{ type: "text", text: `Collection Details:\n- ID: ${getCollection.$id}\n- Name: ${getCollection.name}\n- Attributes: ${getCollection.attributes.length}\n- Indexes: ${getCollection.indexes.length}` }] content: [{ type: "text", text: `Collection Details:\n- ID: ${getCollection.$id}\n- Name: ${getCollection.name}\n- Attributes: ${getCollection.attributes.length}\n- Indexes: ${getCollection.indexes.length}\n- Document Security: ${getCollection.documentSecurity}\n- Enabled: ${getCollection.enabled}` }]
}; };
case "list": case "list":
const collections = await this.databases.listCollections(databaseId); const parsedQueries: string[] = [];
const colList = collections.collections.map(col => `- ${col.name} (${col.$id})`).join('\n'); if (queries && Array.isArray(queries)) {
for (const queryStr of queries) {
parsedQueries.push(this.parseQuery(queryStr));
}
}
if (!parsedQueries.some(q => q.includes('limit'))) {
parsedQueries.push(Query.limit(5000));
}
const collections = await this.databases.listCollections(databaseId, parsedQueries, search);
const colList = collections.collections.map(col => `- ${col.name} (${col.$id}) - ${col.enabled ? 'enabled' : 'disabled'}`).join('\n');
return { return {
content: [{ type: "text", text: `Collections in database ${databaseId} (${collections.total}):\n${colList}` }] content: [{ type: "text", text: `Collections in database ${databaseId} (${collections.total}):\n${colList}` }]
}; };
@@ -662,9 +632,9 @@ class AppwriteMCPServer {
case "update": case "update":
if (!collectionId) throw new Error("collectionId is required for update action"); if (!collectionId) throw new Error("collectionId is required for update action");
if (!name) throw new Error("Name is required for update action"); if (!name) throw new Error("Name is required for update action");
const updatedCollection = await this.databases.updateCollection(databaseId, collectionId, name, permissions); const updatedCollection = await this.databases.updateCollection(databaseId, collectionId, name, permissions, documentSecurity, enabled);
return { return {
content: [{ type: "text", text: `Collection updated successfully:\n- ID: ${updatedCollection.$id}\n- Name: ${updatedCollection.name}` }] content: [{ type: "text", text: `Collection updated successfully:\n- ID: ${updatedCollection.$id}\n- Name: ${updatedCollection.name}\n- Document Security: ${updatedCollection.documentSecurity}\n- Enabled: ${updatedCollection.enabled}` }]
}; };
case "delete": case "delete":
@@ -735,12 +705,26 @@ class AppwriteMCPServer {
case "get": case "get":
if (!key) throw new Error("key is required for get action"); if (!key) throw new Error("key is required for get action");
const attribute = await this.databases.getAttribute(databaseId, collectionId, key); const attribute = await this.databases.getAttribute(databaseId, collectionId, key);
const attr = attribute as any;
// Build detailed attribute information
let details = `Attribute Details:\n- Key: ${attr.key}\n- Type: ${attr.type}\n- Required: ${attr.required}\n- Status: ${attr.status}`;
// Add type-specific details
if (attr.size !== undefined) details += `\n- Size: ${attr.size}`;
if (attr.min !== undefined) details += `\n- Min: ${attr.min}`;
if (attr.max !== undefined) details += `\n- Max: ${attr.max}`;
if (attr.default !== undefined && attr.default !== null) details += `\n- Default: ${attr.default}`;
if (attr.array !== undefined) details += `\n- Array: ${attr.array}`;
if (attr.elements !== undefined) details += `\n- Elements: ${attr.elements.join(', ')}`;
if (attr.format !== undefined) details += `\n- Format: ${attr.format}`;
return { return {
content: [{ type: "text", text: `Attribute Details:\n- Key: ${(attribute as any).key}\n- Type: ${(attribute as any).type}\n- Required: ${(attribute as any).required}\n- Status: ${(attribute as any).status}` }] content: [{ type: "text", text: details }]
}; };
case "list": case "list":
const attributesList = await this.databases.listAttributes(databaseId, collectionId); const attributesList = await this.databases.listAttributes(databaseId, collectionId, [Query.limit(5000)]);
const attrList = attributesList.attributes.map((attr: any) => `- ${attr.key} (${attr.type})`).join('\n'); const attrList = attributesList.attributes.map((attr: any) => `- ${attr.key} (${attr.type})`).join('\n');
return { return {
content: [{ type: "text", text: `Attributes in collection ${collectionId} (${attributesList.total}):\n${attrList}` }] content: [{ type: "text", text: `Attributes in collection ${collectionId} (${attributesList.total}):\n${attrList}` }]
@@ -753,7 +737,16 @@ class AppwriteMCPServer {
let updateResult; let updateResult;
switch (type) { switch (type) {
case "string": case "string":
updateResult = await this.databases.updateStringAttribute(databaseId, collectionId, key, required, otherArgs.default); // Appwrite API requires default param, use null for required attrs if not provided
const strDefault = otherArgs.default !== undefined ? otherArgs.default : null;
updateResult = await this.databases.updateStringAttribute(
databaseId,
collectionId,
key,
required,
strDefault as any,
otherArgs.size
);
break; break;
case "integer": case "integer":
updateResult = await this.databases.updateIntegerAttribute(databaseId, collectionId, key, required, otherArgs.min, otherArgs.max, otherArgs.default); updateResult = await this.databases.updateIntegerAttribute(databaseId, collectionId, key, required, otherArgs.min, otherArgs.max, otherArgs.default);
@@ -861,7 +854,7 @@ class AppwriteMCPServer {
}; };
case "list": case "list":
const indexes = await this.databases.listIndexes(databaseId, collectionId); const indexes = await this.databases.listIndexes(databaseId, collectionId, [Query.limit(5000)]);
const indexList = indexes.indexes.map(idx => `- ${idx.key} (${idx.type}) - [${idx.attributes.join(', ')}]`).join('\n'); const indexList = indexes.indexes.map(idx => `- ${idx.key} (${idx.type}) - [${idx.attributes.join(', ')}]`).join('\n');
return { return {
content: [{ type: "text", text: `Indexes in collection ${collectionId} (${indexes.total}):\n${indexList}` }] content: [{ type: "text", text: `Indexes in collection ${collectionId} (${indexes.total}):\n${indexList}` }]
@@ -971,12 +964,127 @@ class AppwriteMCPServer {
} }
} }
private parseQuery(queryString: string): string {
// Parse query strings like 'equal("field", "value")' into proper Query objects
// This regex matches: functionName("field", value) or functionName("field", [values])
const match = queryString.match(/^(\w+)\((.*)\)$/);
if (!match) {
throw new Error(`Invalid query format: ${queryString}. Expected format: equal("field", "value")`);
}
const [, method, argsStr] = match;
// Parse arguments - handle quoted strings and arrays
const args: any[] = [];
let current = '';
let inQuotes = false;
let inArray = false;
for (let i = 0; i < argsStr.length; i++) {
const char = argsStr[i];
if (char === '"' && argsStr[i - 1] !== '\\') {
inQuotes = !inQuotes;
continue;
}
if (char === '[' && !inQuotes) {
inArray = true;
current += char;
continue;
}
if (char === ']' && !inQuotes) {
inArray = false;
current += char;
continue;
}
if (char === ',' && !inQuotes && !inArray) {
args.push(current.trim());
current = '';
continue;
}
current += char;
}
if (current.trim()) {
args.push(current.trim());
}
// Convert parsed arguments to proper types
const processedArgs = args.map(arg => {
// Handle arrays
if (arg.startsWith('[') && arg.endsWith(']')) {
const items = arg.slice(1, -1).split(',').map((item: string) => item.trim().replace(/^"(.*)"$/, '$1'));
return items;
}
// Handle numbers
if (/^-?\d+(\.\d+)?$/.test(arg)) {
return Number(arg);
}
// Handle booleans
if (arg === 'true') return true;
if (arg === 'false') return false;
// Handle strings (remove quotes)
return arg.replace(/^"(.*)"$/, '$1');
});
// Map to Query methods
const queryMap: Record<string, (...args: any[]) => string> = {
equal: Query.equal,
notEqual: Query.notEqual,
lessThan: Query.lessThan,
lessThanEqual: Query.lessThanEqual,
greaterThan: Query.greaterThan,
greaterThanEqual: Query.greaterThanEqual,
search: Query.search,
isNull: Query.isNull,
isNotNull: Query.isNotNull,
between: Query.between,
startsWith: Query.startsWith,
endsWith: Query.endsWith,
select: Query.select,
orderDesc: Query.orderDesc,
orderAsc: Query.orderAsc,
limit: Query.limit,
offset: Query.offset,
contains: Query.contains,
or: Query.or,
and: Query.and,
};
if (!queryMap[method]) {
throw new Error(`Unknown query method: ${method}. Available methods: ${Object.keys(queryMap).join(', ')}`);
}
return queryMap[method](...processedArgs);
}
private async listDocuments(args: any) { private async listDocuments(args: any) {
if (!this.databases) throw new Error("Databases not initialized"); if (!this.databases) throw new Error("Databases not initialized");
const { databaseId, collectionId, queries, limit, offset } = args; const { databaseId, collectionId, queries, limit, offset } = args;
const documents = await this.databases.listDocuments(databaseId, collectionId, queries || []);
// Parse query strings into Query objects
const parsedQueries: string[] = [];
if (queries && Array.isArray(queries)) {
for (const queryStr of queries) {
try {
parsedQueries.push(this.parseQuery(queryStr));
} catch (error) {
throw new Error(`Failed to parse query "${queryStr}": ${error instanceof Error ? error.message : String(error)}`);
}
}
}
// Add limit and offset if provided
if (limit) parsedQueries.push(Query.limit(limit));
if (offset) parsedQueries.push(Query.offset(offset));
const documents = await this.databases.listDocuments(databaseId, collectionId, parsedQueries);
const docList = documents.documents.map(doc => `- ${doc.$id} (updated: ${doc.$updatedAt})`).join('\n'); const docList = documents.documents.map(doc => `- ${doc.$id} (updated: ${doc.$updatedAt})`).join('\n');
return { return {
content: [{ type: "text", text: `Documents in collection ${collectionId} (${documents.total}):\n${docList}` }] content: [{ type: "text", text: `Documents in collection ${collectionId} (${documents.total}):\n${docList}` }]
@@ -999,8 +1107,24 @@ class AppwriteMCPServer {
case "get": case "get":
if (!userId) throw new Error("userId is required for get action"); if (!userId) throw new Error("userId is required for get action");
const getUser = await this.users.get(userId); const getUser = await this.users.get(userId);
let userDetails = `User Details:\n- ID: ${getUser.$id}\n- Email: ${getUser.email}\n- Name: ${getUser.name || 'N/A'}\n- Status: ${getUser.status}\n- Registration: ${getUser.registration}`;
// Add phone if available
if (getUser.phone) userDetails += `\n- Phone: ${getUser.phone}`;
// Add labels if available
if (getUser.labels && getUser.labels.length > 0) {
userDetails += `\n- Labels: ${getUser.labels.join(', ')}`;
}
// Add preferences if available
if (getUser.prefs && Object.keys(getUser.prefs).length > 0) {
userDetails += `\n- Preferences: ${JSON.stringify(getUser.prefs, null, 2)}`;
}
return { return {
content: [{ type: "text", text: `User Details:\n- ID: ${getUser.$id}\n- Email: ${getUser.email}\n- Name: ${getUser.name || 'N/A'}\n- Status: ${getUser.status}\n- Registration: ${getUser.registration}` }] content: [{ type: "text", text: userDetails }]
}; };
case "update": case "update":
@@ -1098,10 +1222,24 @@ class AppwriteMCPServer {
private async listUsers(args: any) { private async listUsers(args: any) {
if (!this.users) throw new Error("Users not initialized"); if (!this.users) throw new Error("Users not initialized");
const { queries, limit, offset } = args; const { queries, limit, offset } = args;
const users = await this.users.list(); const parsedQueries: string[] = [];
if (queries && Array.isArray(queries)) {
for (const queryStr of queries) {
parsedQueries.push(this.parseQuery(queryStr));
}
}
if (limit) parsedQueries.push(Query.limit(limit));
if (offset) parsedQueries.push(Query.offset(offset));
// If no limit specified, default to 5000 to get all users
if (!limit && !queries) parsedQueries.push(Query.limit(5000));
const users = await this.users.list(parsedQueries);
const userList = users.users.map(user => `- ${user.email} (${user.$id}) - ${user.status}`).join('\n'); const userList = users.users.map(user => `- ${user.email} (${user.$id}) - ${user.status}`).join('\n');
return { return {
content: [{ type: "text", text: `Users (${users.total}):\n${userList}` }] content: [{ type: "text", text: `Users (${users.total}):\n${userList}` }]
@@ -1110,26 +1248,37 @@ class AppwriteMCPServer {
private async bucketOperations(args: any) { private async bucketOperations(args: any) {
if (!this.storage) throw new Error("Storage not initialized"); if (!this.storage) throw new Error("Storage not initialized");
const { action, bucketId, name, permissions, fileSecurity, enabled } = args; const { action, bucketId, name, permissions, fileSecurity, enabled, maximumFileSize, allowedFileExtensions, compression, encryption, antivirus } = args;
switch (action) { switch (action) {
case "create": case "create":
if (!name) throw new Error("name is required for create action"); if (!name) throw new Error("name is required for create action");
const bucket = await this.storage.createBucket(bucketId || ID.unique(), name, permissions, fileSecurity, enabled); const bucket = await this.storage.createBucket(
bucketId || ID.unique(),
name,
permissions,
fileSecurity,
enabled,
maximumFileSize,
allowedFileExtensions,
compression,
encryption,
antivirus
);
return { return {
content: [{ type: "text", text: `Bucket created successfully:\n- ID: ${bucket.$id}\n- Name: ${bucket.name}\n- Enabled: ${bucket.enabled}` }] content: [{ type: "text", text: `Bucket created successfully:\n- ID: ${bucket.$id}\n- Name: ${bucket.name}\n- Enabled: ${bucket.enabled}\n- File Security: ${bucket.fileSecurity}\n- Max File Size: ${bucket.maximumFileSize || 'unlimited'} bytes\n- Compression: ${bucket.compression}\n- Encryption: ${bucket.encryption}\n- Antivirus: ${bucket.antivirus}` }]
}; };
case "get": case "get":
if (!bucketId) throw new Error("bucketId is required for get action"); if (!bucketId) throw new Error("bucketId is required for get action");
const getBucket = await this.storage.getBucket(bucketId); const getBucket = await this.storage.getBucket(bucketId);
return { return {
content: [{ type: "text", text: `Bucket Details:\n- ID: ${getBucket.$id}\n- Name: ${getBucket.name}\n- Enabled: ${getBucket.enabled}\n- File Security: ${getBucket.fileSecurity}` }] content: [{ type: "text", text: `Bucket Details:\n- ID: ${getBucket.$id}\n- Name: ${getBucket.name}\n- Enabled: ${getBucket.enabled}\n- File Security: ${getBucket.fileSecurity}\n- Max File Size: ${getBucket.maximumFileSize || 'unlimited'} bytes\n- Allowed Extensions: ${getBucket.allowedFileExtensions?.join(', ') || 'all'}\n- Compression: ${getBucket.compression}\n- Encryption: ${getBucket.encryption}\n- Antivirus: ${getBucket.antivirus}` }]
}; };
case "list": case "list":
const buckets = await this.storage.listBuckets(); const buckets = await this.storage.listBuckets([Query.limit(5000)]);
const bucketList = buckets.buckets.map(bucket => `- ${bucket.name} (${bucket.$id}) - ${bucket.enabled ? 'enabled' : 'disabled'}`).join('\n'); const bucketList = buckets.buckets.map(bucket => `- ${bucket.name} (${bucket.$id}) - ${bucket.enabled ? 'enabled' : 'disabled'}`).join('\n');
return { return {
content: [{ type: "text", text: `Storage Buckets (${buckets.total}):\n${bucketList}` }] content: [{ type: "text", text: `Storage Buckets (${buckets.total}):\n${bucketList}` }]
@@ -1137,9 +1286,20 @@ class AppwriteMCPServer {
case "update": case "update":
if (!bucketId || !name) throw new Error("bucketId and name are required for update action"); if (!bucketId || !name) throw new Error("bucketId and name are required for update action");
const updatedBucket = await this.storage.updateBucket(bucketId, name, permissions, fileSecurity, enabled); const updatedBucket = await this.storage.updateBucket(
bucketId,
name,
permissions,
fileSecurity,
enabled,
maximumFileSize,
allowedFileExtensions,
compression,
encryption,
antivirus
);
return { return {
content: [{ type: "text", text: `Bucket updated successfully:\n- ID: ${updatedBucket.$id}\n- Name: ${updatedBucket.name}` }] content: [{ type: "text", text: `Bucket updated successfully:\n- ID: ${updatedBucket.$id}\n- Name: ${updatedBucket.name}\n- Enabled: ${updatedBucket.enabled}` }]
}; };
case "delete": case "delete":
@@ -1156,8 +1316,8 @@ class AppwriteMCPServer {
private async fileOperations(args: any) { private async fileOperations(args: any) {
if (!this.storage) throw new Error("Storage not initialized"); if (!this.storage) throw new Error("Storage not initialized");
const { action, bucketId, fileId, filePath, name, permissions } = args; const { action, bucketId, fileId, filePath, downloadPath, name, permissions } = args;
switch (action) { switch (action) {
case "get": case "get":
@@ -1170,14 +1330,35 @@ class AppwriteMCPServer {
case "create": case "create":
if (!filePath) throw new Error("filePath is required for create action"); if (!filePath) throw new Error("filePath is required for create action");
if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`); if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
const fileBuffer = readFileSync(filePath); const fileBuffer = readFileSync(filePath);
const inputFile = InputFile.fromBuffer(fileBuffer, filePath.split('/').pop() || 'file'); const inputFile = InputFile.fromBuffer(fileBuffer, filePath.split(/[/\\]/).pop() || 'file');
const createdFile = await this.storage.createFile(bucketId, fileId || ID.unique(), inputFile, permissions); const createdFile = await this.storage.createFile(bucketId, fileId || ID.unique(), inputFile, permissions);
return { return {
content: [{ type: "text", text: `File uploaded successfully:\n- ID: ${createdFile.$id}\n- Name: ${createdFile.name}\n- Size: ${createdFile.sizeOriginal} bytes` }] content: [{ type: "text", text: `File uploaded successfully:\n- ID: ${createdFile.$id}\n- Name: ${createdFile.name}\n- Size: ${createdFile.sizeOriginal} bytes` }]
}; };
case "download":
if (!fileId) throw new Error("fileId is required for download action");
if (!downloadPath) throw new Error("downloadPath is required for download action");
// Get file metadata first
const downloadFile = await this.storage.getFile(bucketId, fileId);
// Get download URL and fetch the file
const downloadUrl = this.storage.getFileDownload(bucketId, fileId);
// Use fetch to download the file
const response = await fetch(downloadUrl.toString());
if (!response.ok) throw new Error(`Failed to download file: ${response.statusText}`);
const buffer = Buffer.from(await response.arrayBuffer());
writeFileSync(downloadPath, buffer);
return {
content: [{ type: "text", text: `File downloaded successfully:\n- ID: ${downloadFile.$id}\n- Name: ${downloadFile.name}\n- Size: ${downloadFile.sizeOriginal} bytes\n- Saved to: ${downloadPath}` }]
};
case "update": case "update":
if (!fileId) throw new Error("fileId is required for update action"); if (!fileId) throw new Error("fileId is required for update action");
const updatedFile = await this.storage.updateFile(bucketId, fileId, name, permissions); const updatedFile = await this.storage.updateFile(bucketId, fileId, name, permissions);
@@ -1193,7 +1374,7 @@ class AppwriteMCPServer {
}; };
case "list": case "list":
const files = await this.storage.listFiles(bucketId); const files = await this.storage.listFiles(bucketId, [Query.limit(5000)]);
const fileList = files.files.map(file => `- ${file.name} (${file.$id}) - ${file.sizeOriginal} bytes`).join('\n'); const fileList = files.files.map(file => `- ${file.name} (${file.$id}) - ${file.sizeOriginal} bytes`).join('\n');
return { return {
content: [{ type: "text", text: `Files in bucket ${bucketId} (${files.total}):\n${fileList}` }] content: [{ type: "text", text: `Files in bucket ${bucketId} (${files.total}):\n${fileList}` }]
@@ -1254,7 +1435,7 @@ class AppwriteMCPServer {
}; };
case "list": case "list":
const teams = await this.teams.list(); const teams = await this.teams.list([Query.limit(5000)]);
const teamList = teams.teams.map(team => `- ${team.name} (${team.$id}) - ${team.total} members`).join('\n'); const teamList = teams.teams.map(team => `- ${team.name} (${team.$id}) - ${team.total} members`).join('\n');
return { return {
content: [{ type: "text", text: `Teams (${teams.total}):\n${teamList}` }] content: [{ type: "text", text: `Teams (${teams.total}):\n${teamList}` }]
@@ -1279,121 +1460,6 @@ class AppwriteMCPServer {
} }
} }
private async messagingMessageOperations(args: any) {
if (!this.messaging) throw new Error("Messaging not initialized");
const { action, messageId, subject, content, topics, users, targets, status, queries } = args;
switch (action) {
case "create":
if (!content) throw new Error("content is required for create action");
// Note: Using a placeholder - actual Appwrite messaging API may differ
return {
content: [{ type: "text", text: `Message operation not fully implemented in current Appwrite SDK version` }]
};
case "get":
if (!messageId) throw new Error("messageId is required for get action");
return {
content: [{ type: "text", text: `Message operation not fully implemented in current Appwrite SDK version` }]
};
case "list":
return {
content: [{ type: "text", text: `Message operation not fully implemented in current Appwrite SDK version` }]
};
case "update":
if (!messageId) throw new Error("messageId is required for update action");
return {
content: [{ type: "text", text: `Message operation not fully implemented in current Appwrite SDK version` }]
};
case "delete":
if (!messageId) throw new Error("messageId is required for delete action");
return {
content: [{ type: "text", text: `Message operation not fully implemented in current Appwrite SDK version` }]
};
default:
throw new Error(`Unknown action: ${action}`);
}
}
private async messagingTopicOperations(args: any) {
if (!this.messaging) throw new Error("Messaging not initialized");
const { action, topicId, name, description, queries } = args;
switch (action) {
case "create":
if (!name) throw new Error("name is required for create action");
return {
content: [{ type: "text", text: `Topic operation not fully implemented in current Appwrite SDK version` }]
};
case "get":
if (!topicId) throw new Error("topicId is required for get action");
return {
content: [{ type: "text", text: `Topic operation not fully implemented in current Appwrite SDK version` }]
};
case "list":
return {
content: [{ type: "text", text: `Topic operation not fully implemented in current Appwrite SDK version` }]
};
case "update":
if (!topicId) throw new Error("topicId is required for update action");
return {
content: [{ type: "text", text: `Topic operation not fully implemented in current Appwrite SDK version` }]
};
case "delete":
if (!topicId) throw new Error("topicId is required for delete action");
return {
content: [{ type: "text", text: `Topic operation not fully implemented in current Appwrite SDK version` }]
};
default:
throw new Error(`Unknown action: ${action}`);
}
}
private async messagingSubscriberOperations(args: any) {
if (!this.messaging) throw new Error("Messaging not initialized");
const { action, topicId, subscriberId, targetId, queries } = args;
switch (action) {
case "create":
if (!targetId) throw new Error("targetId is required for create action");
return {
content: [{ type: "text", text: `Subscriber operation not fully implemented in current Appwrite SDK version` }]
};
case "get":
if (!subscriberId) throw new Error("subscriberId is required for get action");
return {
content: [{ type: "text", text: `Subscriber operation not fully implemented in current Appwrite SDK version` }]
};
case "list":
return {
content: [{ type: "text", text: `Subscriber operation not fully implemented in current Appwrite SDK version` }]
};
case "delete":
if (!subscriberId) throw new Error("subscriberId is required for delete action");
return {
content: [{ type: "text", text: `Subscriber operation not fully implemented in current Appwrite SDK version` }]
};
default:
throw new Error(`Unknown action: ${action}`);
}
}
private async getHealth() { private async getHealth() {
if (!this.health) throw new Error("Health not initialized"); if (!this.health) throw new Error("Health not initialized");