diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..16e22d6
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+CLOSEBOT_API_KEY=your_closebot_api_key_here
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa0926a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+dist/
+.env
+*.log
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..53c0707
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,7 @@
+FROM node:18-slim
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci --production
+COPY . .
+RUN npm run build
+CMD ["node", "dist/index.js"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f188add
--- /dev/null
+++ b/README.md
@@ -0,0 +1,87 @@
+# CloseBot MCP Server
+
+Full-featured MCP server for the [CloseBot](https://closebot.com) AI chatbot platform. Manage bots, leads, sources, analytics, knowledge base, and more — all from Claude Desktop or any MCP client.
+
+## Features
+
+- **119 tools** across 14 lazy-loaded modules
+- **6 rich UI tool apps** with HTML dashboards
+- **8 tool groups**: Bot Management, Source Management, Lead Management, Analytics & Metrics, Bot Testing, Library & Knowledge Base, Agency & Billing, Configuration
+- **6 visual apps**: Bot Dashboard, Analytics Dashboard, Test Console, Lead Manager, Library Manager, Leaderboard
+- Full TypeScript with types generated from CloseBot's OpenAPI spec
+- Lazy-loaded modules for minimal context usage
+
+## Setup
+
+### 1. Install dependencies
+
+```bash
+npm install
+```
+
+### 2. Build
+
+```bash
+npm run build
+```
+
+### 3. Set your API key
+
+Get your API key from CloseBot's dashboard (Settings → API Keys).
+
+```bash
+export CLOSEBOT_API_KEY=your_api_key_here
+```
+
+### 4. Add to Claude Desktop
+
+Add to your `claude_desktop_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "closebot": {
+ "command": "node",
+ "args": ["/path/to/closebot-mcp/dist/index.js"],
+ "env": {
+ "CLOSEBOT_API_KEY": "your_api_key_here"
+ }
+ }
+ }
+}
+```
+
+## Tool Groups
+
+| Group | Tools | Description |
+|---|---|---|
+| Bot Management | 18 | CRUD bots, AI creation, publish, versioning, templates, source attach |
+| Source Management | 9 | Sources (GHL sub-accounts), calendars, channels, fields, tags |
+| Lead Management | 6 | Search, filter, update leads and lead instances |
+| Analytics & Metrics | 14 | Agency summary, booking graphs, leaderboards, message analytics, logs |
+| Bot Testing | 7 | Test sessions with send/listen, force-step, rollback |
+| Library & KB | 11 | Files, web-scraping, source attachment, content management |
+| Agency & Billing | 18 | Billing, transactions, wallets, usage tracking, re-billing |
+| Configuration | 30 | Personas, FAQs, folders, notifications, live demos, webhooks, API keys |
+
+## Tool Apps
+
+| App | Description |
+|---|---|
+| `bot_dashboard_app` | Grid view of all bots with status, versions, source count |
+| `analytics_dashboard_app` | Agency stats, response/booking/revenue metrics with time range |
+| `test_console_app` | Interactive test session viewer with conversation and controls |
+| `lead_manager_app` | Searchable lead table with fields and conversation data |
+| `library_manager_app` | File list with type indicators, sources, and scrape status |
+| `leaderboard_app` | Global/local rankings by responses, bookings, or contacts |
+
+## Environment Variables
+
+| Variable | Required | Description |
+|---|---|---|
+| `CLOSEBOT_API_KEY` | Yes | Your CloseBot API key |
+| `CLOSEBOT_BASE_URL` | No | Override API base URL (default: `https://api.closebot.com`) |
+
+## License
+
+MIT
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..7d229bc
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1713 @@
+{
+ "name": "closebot-mcp",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "closebot-mcp",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.12.1"
+ },
+ "bin": {
+ "closebot-mcp": "dist/index.js"
+ },
+ "devDependencies": {
+ "@types/node": "^22.15.0",
+ "tsx": "^4.19.0",
+ "typescript": "^5.7.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.9",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
+ "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.25.3",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz",
+ "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "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",
+ "jose": "^6.1.1",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz",
+ "integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==",
+ "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": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "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/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "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.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.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-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "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": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "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/get-tsconfig": {
+ "version": "4.13.1",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.1.tgz",
+ "integrity": "sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "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/hono": {
+ "version": "4.11.7",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
+ "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/jose": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
+ "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-typed": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+ "license": "BSD-2-Clause"
+ },
+ "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.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/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.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "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/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "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/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.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "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.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "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/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": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
+ "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25 || ^4"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3763d33
--- /dev/null
+++ b/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "closebot-mcp",
+ "version": "1.0.0",
+ "description": "MCP server for CloseBot AI chatbot platform API",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "bin": {
+ "closebot-mcp": "./dist/index.js"
+ },
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/index.js",
+ "dev": "tsx src/index.ts",
+ "clean": "rm -rf dist",
+ "prepublishOnly": "npm run build"
+ },
+ "keywords": ["mcp", "closebot", "ai", "chatbot"],
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.12.1"
+ },
+ "devDependencies": {
+ "@types/node": "^22.15.0",
+ "tsx": "^4.19.0",
+ "typescript": "^5.7.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+}
diff --git a/railway.json b/railway.json
new file mode 100644
index 0000000..be44d87
--- /dev/null
+++ b/railway.json
@@ -0,0 +1,4 @@
+{
+ "build": { "builder": "NIXPACKS" },
+ "deploy": { "startCommand": "npm start" }
+}
diff --git a/src/apps/analytics-dashboard.ts b/src/apps/analytics-dashboard.ts
new file mode 100644
index 0000000..a7a715a
--- /dev/null
+++ b/src/apps/analytics-dashboard.ts
@@ -0,0 +1,180 @@
+import { CloseBotClient, err } from "../client.js";
+import type { ToolDefinition, ToolResult, AgencyDashboardSummaryResponse } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "analytics_dashboard_app",
+ description: "Rich analytics dashboard showing agency summary stats, booking trends, and response/revenue metrics with time range support. Returns HTML visualization.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "Optional source ID to filter metrics" },
+ start: { type: "string", description: "Start date for booking graph (ISO 8601). Defaults to 30 days ago." },
+ end: { type: "string", description: "End date for booking graph (ISO 8601). Defaults to now." },
+ resolution: { type: "string", description: "Graph resolution: hourly, daily, monthly. Default: daily" },
+ },
+ },
+ },
+];
+
+function escapeHtml(s: string): string {
+ return s.replace(/&/g, "&").replace(//g, ">");
+}
+
+function pctChange(current: number, last: number): string {
+ if (last === 0) return current > 0 ? "+∞" : "—";
+ const pct = ((current - last) / last) * 100;
+ const sign = pct >= 0 ? "+" : "";
+ const color = pct >= 0 ? "#4ecdc4" : "#ff6b6b";
+ return `${sign}${pct.toFixed(1)}%`;
+}
+
+function renderDashboard(
+ summary: AgencyDashboardSummaryResponse,
+ bookingData: unknown,
+ metricData: unknown
+): string {
+ const cards = [
+ {
+ label: "Responses This Month",
+ value: summary.currentMonthMessageCount ?? 0,
+ change: pctChange(summary.currentMonthMessageCount ?? 0, summary.lastMonthMessageCount ?? 0),
+ icon: "💬",
+ },
+ {
+ label: "Bookings This Month",
+ value: summary.currentMonthSuccessfulBookings ?? 0,
+ change: pctChange(summary.currentMonthSuccessfulBookings ?? 0, summary.lastMonthSuccessfulBookings ?? 0),
+ icon: "📅",
+ },
+ {
+ label: "Active Sources",
+ value: summary.currentMonthActiveSources ?? 0,
+ change: pctChange(summary.currentMonthActiveSources ?? 0, summary.lastMonthActiveSources ?? 0),
+ icon: "📡",
+ },
+ {
+ label: "Contacts This Month",
+ value: summary.currentMonthContacts ?? 0,
+ change: pctChange(summary.currentMonthContacts ?? 0, summary.lastMonthContacts ?? 0),
+ icon: "👤",
+ },
+ {
+ label: "Current Users",
+ value: summary.currentUsers ?? 0,
+ change: "",
+ icon: "👥",
+ },
+ {
+ label: "Total Storage",
+ value: `${((summary.totalStorage ?? 0) / (1024 * 1024)).toFixed(1)} MB`,
+ change: "",
+ icon: "💾",
+ },
+ ];
+
+ const cardHtml = cards
+ .map(
+ (c) => `
+
+
${c.icon}
+
${typeof c.value === "number" ? c.value.toLocaleString() : c.value}
+
${escapeHtml(c.label)}
+ ${c.change ? `
${c.change}
` : ""}
+
`
+ )
+ .join("");
+
+ // Render booking data as a simple text-based bar chart if it's an array
+ let bookingChartHtml = "";
+ if (Array.isArray(bookingData) && bookingData.length > 0) {
+ const maxVal = Math.max(...bookingData.map((d: Record) => (d.count as number) || 0), 1);
+ const bars = bookingData
+ .slice(-14) // last 14 data points
+ .map((d: Record) => {
+ const val = (d.count as number) || 0;
+ const pct = (val / maxVal) * 100;
+ const label = d.label || d.date || d.key || "";
+ return `
+
+
${escapeHtml(String(label))}
+
+
${val}
+
`;
+ })
+ .join("");
+ bookingChartHtml = `
+
+
📊 Booking Trend
+ ${bars}
+ `;
+ }
+
+ // Metric data summary
+ let metricHtml = "";
+ if (metricData && typeof metricData === "object") {
+ metricHtml = `
+
+
📈 Response Metric Data
+
${escapeHtml(JSON.stringify(metricData, null, 2).slice(0, 2000))}
+
`;
+ }
+
+ return `
+
+
📊 Analytics Dashboard
+
+ ${cardHtml}
+
+ ${bookingChartHtml}
+ ${metricHtml}
+
`;
+}
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ const now = new Date();
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ const start = (args.start as string) || thirtyDaysAgo.toISOString();
+ const end = (args.end as string) || now.toISOString();
+ const resolution = (args.resolution as string) || "daily";
+
+ const [summary, bookingData, metricData] = await Promise.all([
+ client.get("/botMetric/agencySummary", {
+ sourceId: args.sourceId,
+ }),
+ client.get("/botMetric/bookingGraph", {
+ start,
+ end,
+ resolution,
+ sourceId: args.sourceId,
+ }),
+ client.get("/botMetric/agencyMetric", {
+ metric: "responses",
+ start,
+ end,
+ resolution,
+ sourceId: args.sourceId,
+ }),
+ ]);
+
+ const html = renderDashboard(summary, bookingData, metricData);
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Analytics: ${summary.currentMonthMessageCount} responses, ${summary.currentMonthSuccessfulBookings} bookings this month`,
+ },
+ ],
+ structuredContent: { type: "html", html },
+ };
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/apps/bot-dashboard.ts b/src/apps/bot-dashboard.ts
new file mode 100644
index 0000000..e2aa020
--- /dev/null
+++ b/src/apps/bot-dashboard.ts
@@ -0,0 +1,135 @@
+import { CloseBotClient, err } from "../client.js";
+import type { ToolDefinition, ToolResult, BotDto } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "bot_dashboard_app",
+ description: "Rich dashboard showing all bots in a grid with status, versions, source count, and details. Returns HTML visualization.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "Optional: show details for a specific bot instead of the grid" },
+ },
+ },
+ },
+];
+
+function escapeHtml(s: string): string {
+ return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+}
+
+function renderBotGrid(bots: BotDto[]): string {
+ const botCards = bots.map((bot) => {
+ const latestVersion = bot.versions?.find((v) => v.published) || bot.versions?.[0];
+ const versionLabel = latestVersion?.version || "draft";
+ const published = latestVersion?.published ? "🟢" : "🟡";
+ const sourceCount = bot.sources?.length || 0;
+ const locked = bot.locked ? "🔒" : "";
+ const fav = bot.favorited ? "⭐" : "";
+ const category = bot.category || "—";
+ const modified = bot.modifiedAt ? new Date(bot.modifiedAt).toLocaleDateString() : "—";
+
+ return `
+
+
+ ${fav} ${escapeHtml(bot.name || "Unnamed")} ${locked}
+ ${escapeHtml(category)}
+
+
+ ${published} v${escapeHtml(versionLabel)}
+ 📡 ${sourceCount} source${sourceCount !== 1 ? "s" : ""}
+
+
Modified: ${escapeHtml(modified)}
+
ID: ${escapeHtml(bot.id || "")}
+ ${bot.followUpActive ? '
↻ Follow-ups active
' : ""}
+ ${bot.tools && bot.tools.length > 0 ? `
🔧 ${bot.tools.length} tool(s)
` : ""}
+
`;
+ }).join("");
+
+ return `
+
+
🤖 Bot Dashboard (${bots.length} bots)
+
+ ${botCards}
+
+
`;
+}
+
+function renderBotDetail(bot: BotDto): string {
+ const versions = (bot.versions || [])
+ .map(
+ (v) =>
+ `| ${escapeHtml(v.version || "")} | ${v.published ? "✅ Published" : "📝 Draft"} | ${escapeHtml(v.name || "—")} | ${v.modifiedAt ? new Date(v.modifiedAt).toLocaleString() : "—"} |
`
+ )
+ .join("");
+
+ const sources = (bot.sources || [])
+ .map(
+ (s) =>
+ `| ${escapeHtml(s.name || "Unnamed")} | ${escapeHtml(s.category || "—")} | ${s.enabled ? "✅" : "❌"} | ${escapeHtml(s.id || "")} |
`
+ )
+ .join("");
+
+ return `
+
+
🤖 ${escapeHtml(bot.name || "Unnamed")}
+
ID: ${escapeHtml(bot.id || "")}
+
+
+
+
${bot.sources?.length || 0}
+
Sources
+
+
+
${bot.versions?.length || 0}
+
Versions
+
+
+
${bot.locked ? "🔒" : "🔓"}
+
${bot.locked ? "Locked" : "Unlocked"}
+
+
+
${bot.followUpActive ? "✅" : "❌"}
+
Follow-ups
+
+
+
+
📋 Versions
+
+ | Version | Status | Name | Modified |
+ ${versions || '| No versions |
'}
+
+
+
📡 Sources
+
+ | Name | Category | Enabled | ID |
+ ${sources || '| No sources attached |
'}
+
+
`;
+}
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ if (args.botId) {
+ const bot = await client.get(`/bot/${args.botId}`);
+ const html = renderBotDetail(bot);
+ return {
+ content: [{ type: "text", text: `Bot details for ${bot.name || bot.id}` }],
+ structuredContent: { type: "html", html },
+ };
+ }
+
+ const bots = await client.get("/bot");
+ const html = renderBotGrid(bots);
+ return {
+ content: [{ type: "text", text: `Dashboard showing ${bots.length} bots` }],
+ structuredContent: { type: "html", html },
+ };
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/apps/lead-manager.ts b/src/apps/lead-manager.ts
new file mode 100644
index 0000000..cc2b80e
--- /dev/null
+++ b/src/apps/lead-manager.ts
@@ -0,0 +1,184 @@
+import { CloseBotClient, err } from "../client.js";
+import type { ToolDefinition, ToolResult, LeadDto, LeadDtoPaginated } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "lead_manager_app",
+ description: "Searchable lead table with fields, conversation snippets, and status indicators. Returns HTML visualization.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "Filter by source ID" },
+ page: { type: "number", description: "Page number (0-indexed)" },
+ pageSize: { type: "number", description: "Page size (default 20, max 100)" },
+ leadId: { type: "string", description: "Show details for a specific lead" },
+ },
+ },
+ },
+];
+
+function escapeHtml(s: string): string {
+ return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+}
+
+function renderLeadTable(data: LeadDtoPaginated): string {
+ const leads = data.results || [];
+ const rows = leads
+ .map((lead) => {
+ const lastMsg = lead.lastMessage
+ ? escapeHtml(lead.lastMessage.slice(0, 50))
+ : '—';
+ const direction =
+ lead.lastMessageDirection === "outbound"
+ ? '→ out'
+ : lead.lastMessageDirection === "inbound"
+ ? '← in'
+ : '—';
+ const time = lead.lastMessageTime
+ ? new Date(lead.lastMessageTime).toLocaleString()
+ : "—";
+ const source = lead.source?.name || "—";
+ const fieldCount = lead.fields?.length || 0;
+ const instanceCount = lead.instances?.length || 0;
+ const failReason = lead.mostRecentFailureReason
+ ? `⚠️ ${escapeHtml(lead.mostRecentFailureReason.slice(0, 40))}
`
+ : "";
+ const tags =
+ lead.tags && lead.tags.length > 0
+ ? lead.tags
+ .slice(0, 3)
+ .map((t) => `${escapeHtml(t)}`)
+ .join(" ")
+ : "";
+
+ return `
+
+ |
+ ${escapeHtml(lead.name || "Unknown")}
+ ${escapeHtml(lead.id || "")}
+ ${tags ? `${tags} ` : ""}
+ |
+ ${escapeHtml(source)} |
+
+ ${direction}
+ ${lastMsg}
+ ${failReason}
+ |
+ ${escapeHtml(time)} |
+ ${fieldCount} |
+ ${instanceCount} |
+
`;
+ })
+ .join("");
+
+ return `
+
+
👥 Lead Manager
+
+ ${data.total ?? leads.length} total leads · Page ${(data.page ?? 0) + 1} · ${data.pageSize ?? 20} per page
+
+
+
+
+
+ | Lead |
+ Source |
+ Last Message |
+ Time |
+ Fields |
+ Bots |
+
+
+
+ ${rows || '| No leads found |
'}
+
+
+
+
`;
+}
+
+function renderLeadDetail(lead: LeadDto): string {
+ const fields = (lead.fields || [])
+ .map(
+ (f) =>
+ `| ${escapeHtml(f.field || "")} | ${escapeHtml(f.value || "—")} |
`
+ )
+ .join("");
+
+ const instances = (lead.instances || [])
+ .map(
+ (i) =>
+ `| ${escapeHtml(i.botId || "")} | v${escapeHtml(i.botVersion || "?")} | ${i.followUpTime ? new Date(i.followUpTime).toLocaleString() : "—"} |
`
+ )
+ .join("");
+
+ const tags = (lead.tags || [])
+ .map((t) => `${escapeHtml(t)}`)
+ .join("");
+
+ return `
+
+
👤 ${escapeHtml(lead.name || "Unknown Lead")}
+
${escapeHtml(lead.id || "")}
+
+
+
+
Source
+
${escapeHtml(lead.source?.name || "—")}
+
+
+
Last Message
+
${escapeHtml((lead.lastMessage || "—").slice(0, 80))}
+
+
+
Contact ID
+
${escapeHtml(lead.contactId || "—")}
+
+
+
+ ${tags ? `
${tags}
` : ""}
+
+ ${lead.mostRecentFailureReason ? `
⚠️ ${escapeHtml(lead.mostRecentFailureReason)}
` : ""}
+
+
📋 Fields (${lead.fields?.length || 0})
+
+ ${fields || '| No fields |
'}
+
+
+
🤖 Bot Instances (${lead.instances?.length || 0})
+
+ | Bot ID | Version | Follow-up |
+ ${instances || '| No instances |
'}
+
+
`;
+}
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ if (args.leadId) {
+ const lead = await client.get(`/lead/${args.leadId}`);
+ const html = renderLeadDetail(lead);
+ return {
+ content: [{ type: "text", text: `Lead details: ${lead.name || lead.id}` }],
+ structuredContent: { type: "html", html },
+ };
+ }
+
+ const data = await client.get("/lead", {
+ page: args.page,
+ pageSize: args.pageSize || 20,
+ sourceId: args.sourceId,
+ });
+ const html = renderLeadTable(data);
+ return {
+ content: [{ type: "text", text: `${data.total ?? (data.results?.length || 0)} leads found` }],
+ structuredContent: { type: "html", html },
+ };
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/apps/leaderboard.ts b/src/apps/leaderboard.ts
new file mode 100644
index 0000000..3e07cde
--- /dev/null
+++ b/src/apps/leaderboard.ts
@@ -0,0 +1,157 @@
+import { CloseBotClient, err } from "../client.js";
+import type { ToolDefinition, ToolResult, LeaderboardResponse } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "leaderboard_app",
+ description:
+ "Rich leaderboard visualization showing global and local rankings by metric (responses, bookings, contacts). Returns HTML.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ metric: {
+ type: "string",
+ enum: ["responses", "bookings", "contacts"],
+ description: "Metric to rank by (default: responses)",
+ },
+ scope: {
+ type: "string",
+ enum: ["global", "local"],
+ description: "Global or local leaderboard (default: global)",
+ },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ numTopLeaders: {
+ type: "integer",
+ description: "Number of top leaders to show (global, default 10)",
+ },
+ numSurrounding: {
+ type: "integer",
+ description: "Number of surrounding agencies (local, default 5)",
+ },
+ },
+ },
+ },
+];
+
+function escapeHtml(s: string): string {
+ return s
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function medalEmoji(rank: number): string {
+ if (rank === 1) return "🥇";
+ if (rank === 2) return "🥈";
+ if (rank === 3) return "🥉";
+ return `#${rank}`;
+}
+
+function renderLeaderboard(
+ entries: LeaderboardResponse[],
+ metric: string,
+ scope: string,
+ start?: string,
+ end?: string
+): string {
+ const metricLabel = metric.charAt(0).toUpperCase() + metric.slice(1);
+ const scopeLabel = scope === "local" ? "Local" : "Global";
+ const dateRange =
+ start && end
+ ? `${new Date(start).toLocaleDateString()} – ${new Date(end).toLocaleDateString()}`
+ : "All time";
+
+ const rows = entries
+ .map((e, i) => {
+ const rank = e.rank ?? i + 1;
+ const medal = medalEmoji(rank);
+ const name = escapeHtml(e.agencyName || e.agencyId || "Unknown");
+ const value = e.value ?? 0;
+ const isYou = e.isCurrentAgency ? ' style="background:#2d3748;font-weight:bold"' : "";
+ const youBadge = e.isCurrentAgency
+ ? ' YOU'
+ : "";
+ return `| ${medal} | ${name}${youBadge} | ${value.toLocaleString()} |
`;
+ })
+ .join("\n");
+
+ return `
+
+
+
${scopeLabel} Leaderboard
+ ${metricLabel} · ${dateRange}
+
+
+ Responses
+ Bookings
+ Contacts
+
+
+
+
+
+ | Rank |
+ Agency |
+ ${metricLabel} |
+
+
+
+ ${rows || '| No leaderboard data available |
'}
+
+
+
${entries.length} agencies shown
+
`;
+}
+
+export async function handle(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ const metric = (args.metric as string) || "responses";
+ const scope = (args.scope as string) || "global";
+ const start = args.start as string | undefined;
+ const end = args.end as string | undefined;
+
+ let entries: LeaderboardResponse[];
+
+ if (scope === "local") {
+ entries = await client.get(
+ "/botMetric/localleaderboard",
+ {
+ metric,
+ start,
+ end,
+ numSurroundingAgencies: args.numSurrounding ?? 5,
+ }
+ );
+ } else {
+ entries = await client.get(
+ "/botMetric/leaderboard",
+ {
+ metric,
+ start,
+ end,
+ numTopLeaders: args.numTopLeaders ?? 10,
+ }
+ );
+ }
+
+ const html = renderLeaderboard(
+ Array.isArray(entries) ? entries : [],
+ metric,
+ scope,
+ start,
+ end
+ );
+
+ return {
+ content: [{ type: "text" as const, text: html }],
+ };
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/apps/library-manager.ts b/src/apps/library-manager.ts
new file mode 100644
index 0000000..b03e082
--- /dev/null
+++ b/src/apps/library-manager.ts
@@ -0,0 +1,192 @@
+import { CloseBotClient, err } from "../client.js";
+import type { ToolDefinition, ToolResult, FileDto } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "library_manager_app",
+ description: "Knowledge base library viewer showing files with type icons, source attachments, scrape status, and size. Returns HTML visualization.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "Optional: show detail for a specific file including scrape pages" },
+ },
+ },
+ },
+];
+
+function escapeHtml(s: string): string {
+ return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+}
+
+function fileIcon(fileType: string | null | undefined): string {
+ const t = (fileType || "").toLowerCase();
+ if (t.includes("pdf")) return "📄";
+ if (t.includes("doc") || t.includes("word")) return "📝";
+ if (t.includes("csv") || t.includes("excel") || t.includes("spreadsheet")) return "📊";
+ if (t.includes("image") || t.includes("png") || t.includes("jpg")) return "🖼️";
+ if (t.includes("webscrape") || t.includes("web") || t.includes("html")) return "🌐";
+ if (t.includes("text") || t.includes("txt")) return "📃";
+ if (t.includes("json")) return "🔧";
+ return "📁";
+}
+
+function formatSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+function statusBadge(status: string | null | undefined): string {
+ const s = (status || "").toLowerCase();
+ if (s === "ready" || s === "completed" || s === "processed")
+ return '✅ Ready';
+ if (s === "processing" || s === "pending")
+ return '⏳ Processing';
+ if (s === "error" || s === "failed")
+ return '❌ Error';
+ return `${escapeHtml(status || "Unknown")}`;
+}
+
+function renderFileList(files: FileDto[]): string {
+ const totalSize = files.reduce((sum, f) => sum + (f.fileSize || 0), 0);
+
+ const rows = files
+ .map((f) => {
+ const icon = fileIcon(f.fileType);
+ const sources = (f.sources || [])
+ .map(
+ (s) =>
+ `${escapeHtml(s.name || s.id || "")}`
+ )
+ .join(" ");
+ const modified = f.lastModified
+ ? new Date(f.lastModified).toLocaleDateString()
+ : "—";
+
+ return `
+
+
${icon}
+
+
+ ${escapeHtml(f.fileName || "Unnamed")}
+
+
+ ${statusBadge(f.fileStatus)}
+ ${escapeHtml(f.fileType || "")}
+ ${formatSize(f.fileSize || 0)}
+ ${escapeHtml(modified)}
+
+ ${sources ? `
${sources}
` : ""}
+
+
${escapeHtml(f.fileId || "")}
+
`;
+ })
+ .join("");
+
+ return `
+
+
📚 Library Manager
+
+ ${files.length} files · ${formatSize(totalSize)} total
+
+
+ ${rows || '
No files. Use upload_file or create_web_scrape to add content.
'}
+
+
+ 💡 Use upload_file to upload, create_web_scrape to scrape websites, attach_file_to_source to connect to sources
+
+
`;
+}
+
+function renderFileDetail(file: FileDto, scrapePages: Array<{ url?: string; enabled?: boolean }>): string {
+ const sources = (file.sources || [])
+ .map(
+ (s) =>
+ `
+ ${escapeHtml(s.name || "Unnamed")}
+ ${escapeHtml(s.category || "")} · ${escapeHtml(s.id || "")}
+
`
+ )
+ .join("");
+
+ const pages = scrapePages
+ .map(
+ (p) =>
+ `
+ ${p.enabled ? "✅" : "❌"}
+ ${escapeHtml(p.url || "")}
+
`
+ )
+ .join("");
+
+ return `
+
+
+
${fileIcon(file.fileType)}
+
+
${escapeHtml(file.fileName || "Unnamed")}
+
${escapeHtml(file.fileId || "")}
+
+
+
+
+
+
Status
+
${statusBadge(file.fileStatus)}
+
+
+
Type
+
${escapeHtml(file.fileType || "—")}
+
+
+
Size
+
${formatSize(file.fileSize || 0)}
+
+
+
Modified
+
${file.lastModified ? new Date(file.lastModified).toLocaleDateString() : "—"}
+
+
+
+
📡 Attached Sources (${file.sources?.length || 0})
+
+ ${sources || '
Not attached to any sources
'}
+
+
+ ${scrapePages.length > 0 ? `
+
🌐 Scrape Pages (${scrapePages.length})
+
+ ${pages}
+
+ ` : ""}
+
`;
+}
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ if (args.fileId) {
+ const [file, scrapePages] = await Promise.all([
+ client.get(`/library/files/${args.fileId}`),
+ client.get>(`/library/files/${args.fileId}/scrape-pages`).catch(() => []),
+ ]);
+ const html = renderFileDetail(file, scrapePages);
+ return {
+ content: [{ type: "text", text: `File: ${file.fileName} (${file.fileType})` }],
+ structuredContent: { type: "html", html },
+ };
+ }
+
+ const files = await client.get("/library/files");
+ const html = renderFileList(files);
+ return {
+ content: [{ type: "text", text: `Library: ${files.length} files` }],
+ structuredContent: { type: "html", html },
+ };
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/apps/test-console.ts b/src/apps/test-console.ts
new file mode 100644
index 0000000..55f7390
--- /dev/null
+++ b/src/apps/test-console.ts
@@ -0,0 +1,145 @@
+import { CloseBotClient, err } from "../client.js";
+import type { ToolDefinition, ToolResult, BotMetricMessage, ListLeadDto } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "test_console_app",
+ description: "Interactive test session viewer showing conversation messages, current step, and session list. Returns HTML visualization.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "The bot ID" },
+ leadId: { type: "string", description: "Optional: specific test session lead ID to view conversation" },
+ },
+ required: ["botId"],
+ },
+ },
+];
+
+function escapeHtml(s: string): string {
+ return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+}
+
+function renderSessionList(sessions: ListLeadDto, botId: string): string {
+ const leads = sessions.leads || [];
+ const rows = leads
+ .map((lead) => {
+ const lastMsg = lead.lastMessage ? escapeHtml(lead.lastMessage.slice(0, 60)) : "—";
+ const time = lead.lastMessageTime
+ ? new Date(lead.lastMessageTime).toLocaleString()
+ : "—";
+ const direction = lead.lastMessageDirection === "outbound" ? "🤖→" : "←👤";
+
+ return `
+
+
+ ${escapeHtml(lead.name || "Test Lead")}
+ ${time}
+
+
${direction} ${lastMsg}${(lead.lastMessage?.length ?? 0) > 60 ? "…" : ""}
+
Lead: ${escapeHtml(lead.id || "")}
+
`;
+ })
+ .join("");
+
+ return `
+
+
🧪 Test Console
+
Bot: ${escapeHtml(botId)} · ${sessions.total ?? leads.length} session(s)
+
+ ${rows || '
No test sessions found. Create one with create_test_session.
'}
+
+
`;
+}
+
+function renderConversation(messages: BotMetricMessage[], botId: string, leadId: string): string {
+ const sorted = [...messages].sort(
+ (a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime()
+ );
+
+ const msgHtml = sorted
+ .map((msg) => {
+ const isBot = msg.fromBot || msg.direction === "outbound";
+ const align = isBot ? "flex-end" : "flex-start";
+ const bg = isBot ? "#1a3a5c" : "#2a1a3e";
+ const border = isBot ? "#2a5a8c" : "#4a2a6e";
+ const label = isBot ? "🤖 Bot" : "👤 User";
+ const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : "";
+ const content = msg.message || "[no content]";
+ const activities =
+ msg.activities && msg.activities.length > 0
+ ? `Activities: ${msg.activities.map((a) => escapeHtml(a.activity || "")).join(", ")}
`
+ : "";
+ const attachments =
+ msg.attachments && msg.attachments.length > 0
+ ? `📎 ${msg.attachments.length} attachment(s)
`
+ : "";
+
+ return `
+
+
+
+ ${label}
+ ${escapeHtml(time)}
+
+
${escapeHtml(content)}
+ ${activities}${attachments}
+
msg: ${escapeHtml(msg.messageId || "")}
+
+
`;
+ })
+ .join("");
+
+ return `
+
+
🧪 Test Conversation
+
+ Bot: ${escapeHtml(botId)} · Lead: ${escapeHtml(leadId)} · ${sorted.length} messages
+
+
+ ${msgHtml || '
No messages yet. Send one with send_test_message.
'}
+
+
+ 💡 Use send_test_message to send messages
+ · force_test_step to advance
+ · rollback_test_session to undo
+
+
`;
+}
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ const botId = args.botId as string;
+ const leadId = args.leadId as string | undefined;
+
+ if (leadId) {
+ const messages = await client.get("/botMetric/messages", {
+ leadId,
+ maxCount: 100,
+ });
+ const html = renderConversation(messages, botId, leadId);
+ return {
+ content: [{ type: "text", text: `Test conversation: ${messages.length} messages` }],
+ structuredContent: { type: "html", html },
+ };
+ }
+
+ const sessions = await client.get(`/bot/${botId}/testSession`, {
+ offset: 0,
+ maxCount: 50,
+ });
+ const html = renderSessionList(sessions, botId);
+ return {
+ content: [
+ { type: "text", text: `${sessions.total ?? (sessions.leads?.length || 0)} test sessions for bot ${botId}` },
+ ],
+ structuredContent: { type: "html", html },
+ };
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/client.ts b/src/client.ts
new file mode 100644
index 0000000..9c6634a
--- /dev/null
+++ b/src/client.ts
@@ -0,0 +1,210 @@
+// ============================================================================
+// CloseBot API HTTP Client
+// ============================================================================
+
+const BASE_URL = "https://api.closebot.com";
+
+export class CloseBotClient {
+ private apiKey: string;
+ private baseUrl: string;
+
+ constructor(apiKey?: string, baseUrl?: string) {
+ this.apiKey = apiKey || process.env.CLOSEBOT_API_KEY || "";
+ this.baseUrl = baseUrl || process.env.CLOSEBOT_BASE_URL || BASE_URL;
+
+ if (!this.apiKey) {
+ throw new Error(
+ "CloseBot API key is required. Set CLOSEBOT_API_KEY environment variable."
+ );
+ }
+ }
+
+ private buildUrl(path: string, query?: Record): string {
+ const url = new URL(path, this.baseUrl);
+ if (query) {
+ for (const [key, value] of Object.entries(query)) {
+ if (value !== undefined && value !== null && value !== "") {
+ url.searchParams.set(key, String(value));
+ }
+ }
+ }
+ return url.toString();
+ }
+
+ private get headers(): Record {
+ return {
+ "X-CB-KEY": this.apiKey,
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ };
+ }
+
+ async get(
+ path: string,
+ query?: Record
+ ): Promise {
+ const url = this.buildUrl(path, query);
+ const response = await fetch(url, {
+ method: "GET",
+ headers: this.headers,
+ });
+ return this.handleResponse(response);
+ }
+
+ async post(
+ path: string,
+ body?: unknown,
+ query?: Record
+ ): Promise {
+ const url = this.buildUrl(path, query);
+ const options: RequestInit = {
+ method: "POST",
+ headers: this.headers,
+ };
+ if (body !== undefined) {
+ options.body = JSON.stringify(body);
+ }
+ const response = await fetch(url, options);
+ return this.handleResponse(response);
+ }
+
+ async put(
+ path: string,
+ body?: unknown,
+ query?: Record
+ ): Promise {
+ const url = this.buildUrl(path, query);
+ const options: RequestInit = {
+ method: "PUT",
+ headers: this.headers,
+ };
+ if (body !== undefined) {
+ options.body = JSON.stringify(body);
+ }
+ const response = await fetch(url, options);
+ return this.handleResponse(response);
+ }
+
+ async delete(
+ path: string,
+ query?: Record
+ ): Promise {
+ const url = this.buildUrl(path, query);
+ const response = await fetch(url, {
+ method: "DELETE",
+ headers: this.headers,
+ });
+ return this.handleResponse(response);
+ }
+
+ async postFormData(
+ path: string,
+ formData: FormData,
+ query?: Record
+ ): Promise {
+ const url = this.buildUrl(path, query);
+ const headers: Record = {
+ "X-CB-KEY": this.apiKey,
+ Accept: "application/json",
+ };
+ const response = await fetch(url, {
+ method: "POST",
+ headers,
+ body: formData,
+ });
+ return this.handleResponse(response);
+ }
+
+ async putFormData(
+ path: string,
+ formData: FormData,
+ query?: Record
+ ): Promise {
+ const url = this.buildUrl(path, query);
+ const headers: Record = {
+ "X-CB-KEY": this.apiKey,
+ Accept: "application/json",
+ };
+ const response = await fetch(url, {
+ method: "PUT",
+ headers,
+ body: formData,
+ });
+ return this.handleResponse(response);
+ }
+
+ private async handleResponse(response: Response): Promise {
+ if (!response.ok) {
+ let errorBody: string;
+ try {
+ errorBody = await response.text();
+ } catch {
+ errorBody = "Unable to read error body";
+ }
+ throw new ApiError(
+ `CloseBot API error ${response.status}: ${response.statusText}`,
+ response.status,
+ errorBody
+ );
+ }
+
+ const contentType = response.headers.get("content-type");
+ if (!contentType || response.status === 204) {
+ return {} as T;
+ }
+
+ if (contentType.includes("application/json") || contentType.includes("text/json")) {
+ return (await response.json()) as T;
+ }
+
+ const text = await response.text();
+ try {
+ return JSON.parse(text) as T;
+ } catch {
+ return text as unknown as T;
+ }
+ }
+}
+
+export class ApiError extends Error {
+ public statusCode: number;
+ public responseBody: string;
+
+ constructor(message: string, statusCode: number, responseBody: string) {
+ super(message);
+ this.name = "ApiError";
+ this.statusCode = statusCode;
+ this.responseBody = responseBody;
+ }
+}
+
+/** Format API result as MCP text content */
+export function ok(data: unknown): {
+ content: Array<{ type: "text"; text: string }>;
+} {
+ return {
+ content: [
+ {
+ type: "text" as const,
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
+ },
+ ],
+ };
+}
+
+/** Format error as MCP error content */
+export function err(error: unknown): {
+ content: Array<{ type: "text"; text: string }>;
+ isError: boolean;
+} {
+ const message =
+ error instanceof ApiError
+ ? `API Error ${error.statusCode}: ${error.message}\n${error.responseBody}`
+ : error instanceof Error
+ ? error.message
+ : String(error);
+ return {
+ content: [{ type: "text" as const, text: message }],
+ isError: true,
+ };
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..35f56df
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,156 @@
+#!/usr/bin/env node
+// ============================================================================
+// CloseBot MCP Server — Main Entry Point
+// Lazy-loaded tool groups, 6 rich UI tool apps, ~55 tools
+// ============================================================================
+
+import { Server } from "@modelcontextprotocol/sdk/server/index.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import {
+ CallToolRequestSchema,
+ ListToolsRequestSchema,
+} from "@modelcontextprotocol/sdk/types.js";
+import { CloseBotClient } from "./client.js";
+import type { ToolDefinition, ToolResult } from "./types.js";
+
+// ---------------------------------------------------------------------------
+// Lazy-loaded module registry
+// ---------------------------------------------------------------------------
+
+interface ToolModule {
+ tools: ToolDefinition[];
+ handle: (
+ client: CloseBotClient,
+ name: string,
+ args: Record
+ ) => Promise;
+}
+
+interface LazyGroup {
+ /** File path (relative) for dynamic import */
+ path: string;
+ /** Cached module after first load */
+ module?: ToolModule;
+ /** Tool metadata — populated eagerly, handler loaded lazily */
+ toolNames: string[];
+}
+
+// Tool groups — we import metadata eagerly but handler code lazily
+const groups: LazyGroup[] = [
+ { path: "./tools/bot-management.js", toolNames: [] },
+ { path: "./tools/source-management.js", toolNames: [] },
+ { path: "./tools/lead-management.js", toolNames: [] },
+ { path: "./tools/analytics.js", toolNames: [] },
+ { path: "./tools/bot-testing.js", toolNames: [] },
+ { path: "./tools/library.js", toolNames: [] },
+ { path: "./tools/agency-billing.js", toolNames: [] },
+ { path: "./tools/configuration.js", toolNames: [] },
+ // Apps
+ { path: "./apps/bot-dashboard.js", toolNames: [] },
+ { path: "./apps/analytics-dashboard.js", toolNames: [] },
+ { path: "./apps/test-console.js", toolNames: [] },
+ { path: "./apps/lead-manager.js", toolNames: [] },
+ { path: "./apps/library-manager.js", toolNames: [] },
+ { path: "./apps/leaderboard.js", toolNames: [] },
+];
+
+// Map: toolName → group index (for fast dispatch)
+const toolToGroup = new Map();
+// All tool definitions (populated on init)
+let allTools: ToolDefinition[] = [];
+
+async function loadGroupMetadata(): Promise {
+ const toolDefs: ToolDefinition[] = [];
+ for (let i = 0; i < groups.length; i++) {
+ const mod = (await import(groups[i].path)) as ToolModule;
+ groups[i].module = mod;
+ groups[i].toolNames = mod.tools.map((t) => t.name);
+ for (const tool of mod.tools) {
+ toolToGroup.set(tool.name, i);
+ toolDefs.push(tool);
+ }
+ }
+ allTools = toolDefs;
+}
+
+async function getHandler(
+ toolName: string
+): Promise {
+ const idx = toolToGroup.get(toolName);
+ if (idx === undefined) return null;
+ const group = groups[idx];
+ if (!group.module) {
+ group.module = (await import(group.path)) as ToolModule;
+ }
+ return group.module.handle;
+}
+
+// ---------------------------------------------------------------------------
+// Server setup
+// ---------------------------------------------------------------------------
+
+async function main(): Promise {
+ // Init client (validates API key)
+ let client: CloseBotClient;
+ try {
+ client = new CloseBotClient();
+ } catch (e) {
+ console.error(
+ (e as Error).message ||
+ "Failed to initialize. Set CLOSEBOT_API_KEY env var."
+ );
+ process.exit(1);
+ }
+
+ // Load all tool metadata
+ await loadGroupMetadata();
+
+ const server = new Server(
+ {
+ name: "closebot-mcp",
+ version: "1.0.0",
+ },
+ {
+ capabilities: {
+ tools: {},
+ },
+ }
+ );
+
+ // --- List Tools ---
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
+ return {
+ tools: allTools.map((t) => ({
+ name: t.name,
+ description: t.description,
+ inputSchema: t.inputSchema,
+ })),
+ };
+ });
+
+ // --- Call Tool ---
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
+ const { name, arguments: args } = request.params;
+ const handler = await getHandler(name);
+ if (!handler) {
+ return {
+ content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
+ isError: true,
+ } as Record;
+ }
+ const result = await handler(client, name, (args as Record) || {});
+ return result as unknown as Record;
+ });
+
+ // --- Connect stdio transport ---
+ const transport = new StdioServerTransport();
+ await server.connect(transport);
+ console.error(
+ `CloseBot MCP server running — ${allTools.length} tools loaded across ${groups.length} modules`
+ );
+}
+
+main().catch((err) => {
+ console.error("Fatal error:", err);
+ process.exit(1);
+});
diff --git a/src/tools/agency-billing.ts b/src/tools/agency-billing.ts
new file mode 100644
index 0000000..97d5f6a
--- /dev/null
+++ b/src/tools/agency-billing.ts
@@ -0,0 +1,353 @@
+import { CloseBotClient, ok, err } from "../client.js";
+import type { ToolDefinition, ToolResult, AgencyDto, BalanceDto, BillingOptionsDto, UpdateBillingConfigInput, ReBillingDto, ReBillingUpdateInput, TransactionDto, BilledUsageDto, AddSourceTransactionDto } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "get_agency",
+ description: "Get the current agency details",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "list_agencies",
+ description: "List all agencies the account is part of (currently max 1)",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "get_usage",
+ description: "Get agency usage across scopes: bots, storage, users, responses",
+ inputSchema: {
+ type: "object",
+ properties: {
+ scopes: {
+ type: "string",
+ description: "Comma-separated scopes (default: bots,storage,users,responses)",
+ },
+ },
+ },
+ },
+ {
+ name: "invite_users",
+ description: "Invite users to the agency",
+ inputSchema: {
+ type: "object",
+ properties: {
+ invites: {
+ type: "array",
+ description: "Array of invite objects",
+ items: {
+ type: "object",
+ properties: {
+ email: { type: "string", description: "Email to invite" },
+ role: { type: "string", description: "Role (admin, member, etc.)" },
+ sourceIds: { type: "array", items: { type: "string" }, description: "Source IDs to give access" },
+ },
+ },
+ },
+ },
+ required: ["invites"],
+ },
+ },
+ {
+ name: "revoke_invitation",
+ description: "Revoke an invitation by email",
+ inputSchema: {
+ type: "object",
+ properties: {
+ email: { type: "string", description: "The email to revoke invitation for" },
+ },
+ required: ["email"],
+ },
+ },
+ {
+ name: "get_balance",
+ description: "Get the agency wallet balance",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "get_source_balance",
+ description: "Get the balance for a specific source",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "The source ID" },
+ },
+ required: ["sourceId"],
+ },
+ },
+ {
+ name: "get_billing_options",
+ description: "Get agency billing configuration (auto-refill, thresholds, etc.)",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "update_billing",
+ description: "Update agency billing configuration",
+ inputSchema: {
+ type: "object",
+ properties: {
+ overBillingEnabled: { type: "boolean", description: "Enable over-billing" },
+ autoRefillEnabled: { type: "boolean", description: "Enable auto-refill" },
+ topUpAmount: { type: "number", description: "Top-up amount (smallest currency unit)" },
+ refillThreshold: { type: "number", description: "Refill threshold (smallest currency unit)" },
+ },
+ },
+ },
+ {
+ name: "list_transactions",
+ description: "List agency transactions with optional status filter and pagination",
+ inputSchema: {
+ type: "object",
+ properties: {
+ status: { type: "string", description: "Filter by status: pending, succeeded, failed" },
+ page: { type: "number", description: "Page number (default 1)" },
+ pageSize: { type: "number", description: "Page size (default 20)" },
+ },
+ },
+ },
+ {
+ name: "list_source_transactions",
+ description: "List transactions for a specific source",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "The source ID" },
+ status: { type: "string", description: "Filter: pending, succeeded, failed" },
+ page: { type: "number", description: "Page number" },
+ pageSize: { type: "number", description: "Page size" },
+ },
+ required: ["sourceId"],
+ },
+ },
+ {
+ name: "add_source_transaction",
+ description: "Add a manual transaction to a source wallet",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "The source ID" },
+ amount: { type: "string", description: "Transaction amount" },
+ description: { type: "string", description: "Transaction description" },
+ },
+ required: ["sourceId", "amount"],
+ },
+ },
+ {
+ name: "delete_source_transaction",
+ description: "Delete a source transaction",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "The source ID" },
+ id: { type: "number", description: "Transaction ID" },
+ },
+ required: ["sourceId", "id"],
+ },
+ },
+ {
+ name: "list_billed_usages",
+ description: "List billed usage records for a time range",
+ inputSchema: {
+ type: "object",
+ properties: {
+ startTime: { type: "string", description: "Start time (ISO 8601)" },
+ endTime: { type: "string", description: "End time (ISO 8601)" },
+ },
+ },
+ },
+ {
+ name: "get_rebilling",
+ description: "Get re-billing settings (response, storage, user costs and token multiplier)",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "update_rebilling",
+ description: "Update re-billing settings",
+ inputSchema: {
+ type: "object",
+ properties: {
+ enabled: { type: "boolean", description: "Enable re-billing" },
+ responseCost: { type: "string", description: "Response unit cost" },
+ storageCost: { type: "string", description: "Storage unit cost" },
+ userCost: { type: "string", description: "User unit cost" },
+ tokenMultiplier: { type: "string", description: "Token multiplier" },
+ },
+ },
+ },
+ {
+ name: "refill_agency_wallet",
+ description: "⚠️ Charges your payment method to refill the agency wallet. Use with caution.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ amount: { type: "number", description: "Amount to refill (smallest currency unit)" },
+ currency: { type: "string", description: "Currency code (e.g., usd)" },
+ },
+ required: ["amount"],
+ },
+ },
+ {
+ name: "refill_source_wallet",
+ description: "⚠️ Charges your payment method to refill a source wallet. Use with caution.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "The source ID" },
+ amount: { type: "number", description: "Amount to refill" },
+ currency: { type: "string", description: "Currency code" },
+ },
+ required: ["sourceId", "amount"],
+ },
+ },
+];
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ switch (name) {
+ case "get_agency":
+ return ok(await client.get("/agency/current"));
+
+ case "list_agencies":
+ return ok(await client.get("/agency"));
+
+ case "get_usage":
+ return ok(
+ await client.get("/agency/usage", { scopes: args.scopes })
+ );
+
+ case "invite_users":
+ return ok(
+ await client.post("/agency/invite", { invites: args.invites })
+ );
+
+ case "revoke_invitation":
+ return ok(
+ await client.delete("/agency/invite", { email: args.email })
+ );
+
+ case "get_balance":
+ return ok(await client.get("/agency/billing/balance"));
+
+ case "get_source_balance":
+ return ok(
+ await client.get(
+ `/agency/billing/balance/source/${args.sourceId}`
+ )
+ );
+
+ case "get_billing_options":
+ return ok(
+ await client.get("/agency/billing/options")
+ );
+
+ case "update_billing": {
+ const body: UpdateBillingConfigInput = {};
+ if (args.overBillingEnabled !== undefined)
+ body.overBillingEnabled = args.overBillingEnabled as boolean;
+ if (args.autoRefillEnabled !== undefined)
+ body.autoRefillEnabled = args.autoRefillEnabled as boolean;
+ if (args.topUpAmount !== undefined)
+ body.topUpAmount = args.topUpAmount as number;
+ if (args.refillThreshold !== undefined)
+ body.refillThreshold = args.refillThreshold as number;
+ return ok(
+ await client.put("/agency/billing/options", body)
+ );
+ }
+
+ case "list_transactions":
+ return ok(
+ await client.get("/agency/billing/transactions", {
+ status: args.status,
+ page: args.page,
+ pageSize: args.pageSize,
+ })
+ );
+
+ case "list_source_transactions":
+ return ok(
+ await client.get(
+ `/agency/billing/transactions/source/${args.sourceId}`,
+ {
+ status: args.status,
+ page: args.page,
+ pageSize: args.pageSize,
+ }
+ )
+ );
+
+ case "add_source_transaction": {
+ const body: AddSourceTransactionDto = {
+ amount: args.amount as string,
+ description: args.description as string | undefined,
+ };
+ return ok(
+ await client.post(
+ `/agency/billing/transactions/source/${args.sourceId}`,
+ body
+ )
+ );
+ }
+
+ case "delete_source_transaction":
+ return ok(
+ await client.delete(
+ `/agency/billing/transactions/source/${args.sourceId}/${args.id}`
+ )
+ );
+
+ case "list_billed_usages":
+ return ok(
+ await client.get("/agency/billing/usages", {
+ startTime: args.startTime,
+ endTime: args.endTime,
+ })
+ );
+
+ case "get_rebilling":
+ return ok(
+ await client.get("/agency/billing/re-billing")
+ );
+
+ case "update_rebilling": {
+ const body: ReBillingUpdateInput = {};
+ if (args.enabled !== undefined) body.enabled = args.enabled as boolean;
+ if (args.responseCost !== undefined)
+ body.responseCost = args.responseCost as string;
+ if (args.storageCost !== undefined)
+ body.storageCost = args.storageCost as string;
+ if (args.userCost !== undefined)
+ body.userCost = args.userCost as string;
+ if (args.tokenMultiplier !== undefined)
+ body.tokenMultiplier = args.tokenMultiplier as string;
+ return ok(
+ await client.put("/agency/billing/re-billing", body)
+ );
+ }
+
+ case "refill_agency_wallet":
+ return ok(
+ await client.post("/agency/billing/refill", {
+ amount: args.amount,
+ currency: args.currency,
+ })
+ );
+
+ case "refill_source_wallet":
+ return ok(
+ await client.post(
+ `/agency/billing/refill/source/${args.sourceId}`,
+ { amount: args.amount, currency: args.currency }
+ )
+ );
+
+ default:
+ return err(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/tools/analytics.ts b/src/tools/analytics.ts
new file mode 100644
index 0000000..a53f386
--- /dev/null
+++ b/src/tools/analytics.ts
@@ -0,0 +1,344 @@
+import { CloseBotClient, ok, err } from "../client.js";
+import type { ToolDefinition, ToolResult, AgencyDashboardSummaryResponse, BotMetricAction, BotMetricMessage, BotMetricLog, LeaderboardResponse, MessageFeedbackRequest, MessageFeedbackResponse, MessageLikesResponse, MessageReason } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "get_agency_summary",
+ description: "Get agency dashboard summary with message counts, bookings, sources, contacts, and storage",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "Optional source ID to filter by" },
+ },
+ },
+ },
+ {
+ name: "get_agency_metric",
+ description: "Get a specific agency metric over time. Metrics: responses, bookings, activeSources, contacts, totalStorage, revenue",
+ inputSchema: {
+ type: "object",
+ properties: {
+ metric: { type: "string", description: "Metric name: responses, bookings, activeSources, contacts, totalStorage, revenue" },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ resolution: { type: "string", description: "Time resolution (hourly, daily, monthly)" },
+ sourceId: { type: "string", description: "Optional source ID" },
+ },
+ required: ["metric", "start", "end"],
+ },
+ },
+ {
+ name: "get_booking_graph",
+ description: "Get booking graph data over a time range",
+ inputSchema: {
+ type: "object",
+ properties: {
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ resolution: { type: "string", description: "Resolution: hourly, daily, or monthly" },
+ sourceId: { type: "string", description: "Optional source ID" },
+ },
+ required: ["start", "end"],
+ },
+ },
+ {
+ name: "get_action_count",
+ description: "Get the count of bot actions, optionally filtered by lead, source, bot, and date range",
+ inputSchema: {
+ type: "object",
+ properties: {
+ leadId: { type: "string", description: "Optional lead ID" },
+ sourceId: { type: "string", description: "Optional source ID" },
+ botId: { type: "string", description: "Optional bot ID" },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ },
+ },
+ },
+ {
+ name: "get_actions",
+ description: "Get detailed bot actions with optional filters",
+ inputSchema: {
+ type: "object",
+ properties: {
+ leadId: { type: "string", description: "Optional lead ID" },
+ sourceId: { type: "string", description: "Optional source ID" },
+ botId: { type: "string", description: "Optional bot ID" },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ maxCount: { type: "number", description: "Maximum number of actions to return" },
+ },
+ },
+ },
+ {
+ name: "get_message_count",
+ description: "Get message count, optionally filtered by source, lead, and date range",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "Optional source ID" },
+ leadId: { type: "string", description: "Optional lead ID" },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ },
+ },
+ },
+ {
+ name: "get_messages",
+ description: "Get messages with optional filters. Returns message content, direction, attachments, and activities.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "Optional source ID" },
+ leadId: { type: "string", description: "Optional lead ID" },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ maxCount: { type: "number", description: "Maximum messages to return" },
+ },
+ },
+ },
+ {
+ name: "get_message_feedback",
+ description: "Get message IDs that have feedback for a lead",
+ inputSchema: {
+ type: "object",
+ properties: {
+ leadId: { type: "string", description: "The lead ID" },
+ },
+ required: ["leadId"],
+ },
+ },
+ {
+ name: "save_message_feedback",
+ description: "Save feedback (like/dislike with reasons) for a specific message",
+ inputSchema: {
+ type: "object",
+ properties: {
+ messageId: { type: "string", description: "The message ID" },
+ leadId: { type: "string", description: "The lead ID" },
+ liked: { type: "boolean", description: "Whether the message was liked" },
+ reasons: { type: "string", description: "Feedback reasons" },
+ },
+ required: ["messageId", "leadId"],
+ },
+ },
+ {
+ name: "get_message_likes",
+ description: "Get message IDs that have likes for a lead",
+ inputSchema: {
+ type: "object",
+ properties: {
+ leadId: { type: "string", description: "The lead ID" },
+ },
+ required: ["leadId"],
+ },
+ },
+ {
+ name: "get_message_reason",
+ description: "Get the feedback reason for a specific message",
+ inputSchema: {
+ type: "object",
+ properties: {
+ messageId: { type: "string", description: "The message ID" },
+ },
+ required: ["messageId"],
+ },
+ },
+ {
+ name: "get_leaderboard",
+ description: "Get the global leaderboard. Metrics: responses, bookings, contacts",
+ inputSchema: {
+ type: "object",
+ properties: {
+ metric: { type: "string", description: "Metric: responses, bookings, contacts" },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ numTopLeaders: { type: "number", description: "Number of top leaders to return" },
+ },
+ required: ["metric", "start", "end"],
+ },
+ },
+ {
+ name: "get_local_leaderboard",
+ description: "Get the local leaderboard (your agency + surrounding). Metrics: responses, bookings, contacts",
+ inputSchema: {
+ type: "object",
+ properties: {
+ metric: { type: "string", description: "Metric: responses, bookings, contacts" },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ numSurroundingAgencies: { type: "number", description: "Number of surrounding agencies" },
+ },
+ required: ["metric", "start", "end"],
+ },
+ },
+ {
+ name: "get_logs",
+ description: "Get bot execution logs with optional filters. Includes prompts, tokens, model info.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "Optional bot ID" },
+ messageId: { type: "string", description: "Optional message ID" },
+ sourceId: { type: "string", description: "Optional source ID" },
+ leadId: { type: "string", description: "Optional lead ID" },
+ actionId: { type: "string", description: "Optional action ID" },
+ start: { type: "string", description: "Start date (ISO 8601)" },
+ end: { type: "string", description: "End date (ISO 8601)" },
+ maxCount: { type: "number", description: "Maximum logs to return" },
+ },
+ },
+ },
+];
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ switch (name) {
+ case "get_agency_summary":
+ return ok(
+ await client.get("/botMetric/agencySummary", {
+ sourceId: args.sourceId,
+ })
+ );
+
+ case "get_agency_metric":
+ return ok(
+ await client.get("/botMetric/agencyMetric", {
+ metric: args.metric,
+ start: args.start,
+ end: args.end,
+ resolution: args.resolution,
+ sourceId: args.sourceId,
+ })
+ );
+
+ case "get_booking_graph":
+ return ok(
+ await client.get("/botMetric/bookingGraph", {
+ start: args.start,
+ end: args.end,
+ resolution: args.resolution,
+ sourceId: args.sourceId,
+ })
+ );
+
+ case "get_action_count":
+ return ok(
+ await client.get("/botMetric/actionCount", {
+ leadId: args.leadId,
+ sourceId: args.sourceId,
+ botId: args.botId,
+ start: args.start,
+ end: args.end,
+ })
+ );
+
+ case "get_actions":
+ return ok(
+ await client.get("/botMetric/actions", {
+ leadId: args.leadId,
+ sourceId: args.sourceId,
+ botId: args.botId,
+ start: args.start,
+ end: args.end,
+ maxCount: args.maxCount,
+ })
+ );
+
+ case "get_message_count":
+ return ok(
+ await client.get("/botMetric/messageCount", {
+ sourceId: args.sourceId,
+ leadId: args.leadId,
+ start: args.start,
+ end: args.end,
+ })
+ );
+
+ case "get_messages":
+ return ok(
+ await client.get("/botMetric/messages", {
+ sourceId: args.sourceId,
+ leadId: args.leadId,
+ start: args.start,
+ end: args.end,
+ maxCount: args.maxCount,
+ })
+ );
+
+ case "get_message_feedback":
+ return ok(
+ await client.get("/botMetric/messageFeedback", {
+ leadId: args.leadId,
+ })
+ );
+
+ case "save_message_feedback": {
+ const body: MessageFeedbackRequest = {
+ messageId: args.messageId as string,
+ leadId: args.leadId as string,
+ liked: args.liked as boolean | undefined,
+ reasons: args.reasons as string | undefined,
+ };
+ return ok(await client.post("/botMetric/messageFeedback", body));
+ }
+
+ case "get_message_likes":
+ return ok(
+ await client.get("/botMetric/messageLikes", {
+ leadId: args.leadId,
+ })
+ );
+
+ case "get_message_reason":
+ return ok(
+ await client.get("/botMetric/messageReason", {
+ messageId: args.messageId,
+ })
+ );
+
+ case "get_leaderboard":
+ return ok(
+ await client.get("/botMetric/leaderboard", {
+ metric: args.metric,
+ start: args.start,
+ end: args.end,
+ numTopLeaders: args.numTopLeaders,
+ })
+ );
+
+ case "get_local_leaderboard":
+ return ok(
+ await client.get("/botMetric/localleaderboard", {
+ metric: args.metric,
+ start: args.start,
+ end: args.end,
+ numSurroundingAgencies: args.numSurroundingAgencies,
+ })
+ );
+
+ case "get_logs":
+ return ok(
+ await client.get("/botMetric/logs", {
+ botId: args.botId,
+ messageId: args.messageId,
+ sourceId: args.sourceId,
+ leadId: args.leadId,
+ actionId: args.actionId,
+ start: args.start,
+ end: args.end,
+ maxCount: args.maxCount,
+ })
+ );
+
+ default:
+ return err(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/tools/bot-management.ts b/src/tools/bot-management.ts
new file mode 100644
index 0000000..fc5a56c
--- /dev/null
+++ b/src/tools/bot-management.ts
@@ -0,0 +1,353 @@
+import { CloseBotClient, ok, err } from "../client.js";
+import type { ToolDefinition, ToolResult, BotDto, CreateBotInput, AiCreateBotInput, UpdateBotInput, ExportBotResponse, SaveBotInput, SaveBotResponse, PublishBotResponse, ToolInputDto, BotToolDto, BotTemplateDto, UpdateVersionInput } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "list_bots",
+ description: "List all bots in the agency",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "get_bot",
+ description: "Get a specific bot by ID",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "create_bot",
+ description: "Create a new bot. Can use a template ID or import KDL.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "The name of the new bot" },
+ templateId: { type: "string", description: "Template ID to create from" },
+ importKdl: { type: "string", description: "KDL template to import" },
+ folderId: { type: "string", description: "Folder ID to place the bot in" },
+ category: { type: "string", description: "Bot category (GHL, WebHook, etc.)" },
+ },
+ required: ["name"],
+ },
+ },
+ {
+ name: "create_bot_with_ai",
+ description: "Create a new bot using AI-generated steps from a text description",
+ inputSchema: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "The name of the new bot" },
+ description: { type: "string", description: "Prompt describing what the bot should do" },
+ category: { type: "string", description: "Source category (GHL, WebHook, etc.)" },
+ folderId: { type: "string", description: "Folder ID to place the bot in" },
+ },
+ required: ["name", "description"],
+ },
+ },
+ {
+ name: "update_bot",
+ description: "Update bot fields. Only non-null fields are updated.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ name: { type: "string", description: "New name" },
+ favorite: { type: "boolean", description: "Whether the bot is favorited" },
+ trash: { type: "boolean", description: "Whether the bot is trashed" },
+ locked: { type: "boolean", description: "Whether the bot is locked" },
+ rescheduling: { type: "boolean", description: "Enable conversation rescheduling" },
+ folderId: { type: "string", description: "Folder ID" },
+ category: { type: "string", description: "Category" },
+ followUpActive: { type: "boolean", description: "Enable follow-ups" },
+ smartFollowUp: { type: "boolean", description: "Enable smart follow-ups" },
+ followUpRepeat: { type: "boolean", description: "Repeat last follow-up sequence" },
+ followUpVarianceMinutes: { type: "number", description: "Variance minutes for follow-ups" },
+ followUpExtraPrompt: { type: "string", description: "Extra prompt for follow-ups" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "delete_bot",
+ description: "Delete a bot by ID",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "duplicate_bot",
+ description: "Duplicate an existing bot",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID to duplicate" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "export_bot",
+ description: "Export a bot as KDL. Optionally specify a version.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ botVersion: { type: "string", description: "Version (x.y.z). Defaults to latest." },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "publish_bot",
+ description: "Publish a bot to make it live",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "save_bot",
+ description: "Save bot steps (the bot flow definition)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ botSteps: { type: "object", description: "The bot steps/flow data" },
+ },
+ required: ["id", "botSteps"],
+ },
+ },
+ {
+ name: "save_bot_tools",
+ description: "Save the tools configuration for a bot",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ tools: {
+ type: "array",
+ description: "Array of tool configurations",
+ items: {
+ type: "object",
+ properties: {
+ type: { type: "string", description: "Tool type" },
+ enabled: { type: "boolean", description: "Whether enabled" },
+ options: { type: "object", description: "Tool options" },
+ },
+ },
+ },
+ },
+ required: ["id", "tools"],
+ },
+ },
+ {
+ name: "get_bot_steps",
+ description: "Get the steps/flow for a specific bot version",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ botVersion: { type: "string", description: "Version (x.y.z)" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "get_node_descriptors",
+ description: "Get all available node descriptors for the bot builder",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "update_bot_version",
+ description: "Update a specific bot version (rename or import KDL)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ version: { type: "string", description: "The version ID" },
+ name: { type: "string", description: "New version name" },
+ importKdl: { type: "string", description: "KDL to import into this version" },
+ },
+ required: ["id", "version"],
+ },
+ },
+ {
+ name: "get_bot_templates",
+ description: "List all available bot templates",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "attach_source_to_bot",
+ description: "Attach a source to a bot with optional tag/channel config",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ sourceId: { type: "string", description: "The source ID" },
+ enabled: { type: "boolean", description: "Whether the attachment is enabled" },
+ channels: { type: "array", items: { type: "string" }, description: "Channel list" },
+ personaNameOverride: { type: "string", description: "Persona name override" },
+ },
+ required: ["id", "sourceId"],
+ },
+ },
+ {
+ name: "detach_source_from_bot",
+ description: "Detach a source from a bot",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The bot ID" },
+ sourceId: { type: "string", description: "The source ID" },
+ },
+ required: ["id", "sourceId"],
+ },
+ },
+ {
+ name: "get_bbb_templates",
+ description: "Get bot builder builder template names",
+ inputSchema: { type: "object", properties: {} },
+ },
+];
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ switch (name) {
+ case "list_bots":
+ return ok(await client.get("/bot"));
+
+ case "get_bot":
+ return ok(await client.get(`/bot/${args.id}`));
+
+ case "create_bot": {
+ const body: CreateBotInput = {
+ name: args.name as string,
+ templateId: args.templateId as string | undefined,
+ importKdl: args.importKdl as string | undefined,
+ folderId: args.folderId as string | undefined,
+ category: args.category as string | undefined,
+ };
+ return ok(await client.post("/bot", body));
+ }
+
+ case "create_bot_with_ai": {
+ const body: AiCreateBotInput = {
+ name: args.name as string,
+ description: args.description as string,
+ category: args.category as string | undefined,
+ folderId: args.folderId as string | undefined,
+ };
+ return ok(await client.post("/bot/ai", body));
+ }
+
+ case "update_bot": {
+ const { id, ...rest } = args;
+ const body: UpdateBotInput = {};
+ if (rest.name !== undefined) body.name = rest.name as string;
+ if (rest.favorite !== undefined) body.favorite = rest.favorite as boolean;
+ if (rest.trash !== undefined) body.trash = rest.trash as boolean;
+ if (rest.locked !== undefined) body.locked = rest.locked as boolean;
+ if (rest.rescheduling !== undefined) body.rescheduling = rest.rescheduling as boolean;
+ if (rest.folderId !== undefined) body.folderId = rest.folderId as string;
+ if (rest.category !== undefined) body.category = rest.category as string;
+ if (rest.followUpActive !== undefined) body.followUpActive = rest.followUpActive as boolean;
+ if (rest.smartFollowUp !== undefined) body.smartFollowUp = rest.smartFollowUp as boolean;
+ if (rest.followUpRepeat !== undefined) body.followUpRepeat = rest.followUpRepeat as boolean;
+ if (rest.followUpVarianceMinutes !== undefined)
+ body.followUpVarianceMinutes = rest.followUpVarianceMinutes as number;
+ if (rest.followUpExtraPrompt !== undefined)
+ body.followUpExtraPrompt = rest.followUpExtraPrompt as string;
+ return ok(await client.put(`/bot/${id}`, body));
+ }
+
+ case "delete_bot":
+ return ok(await client.delete(`/bot/${args.id}`));
+
+ case "duplicate_bot":
+ return ok(await client.post(`/bot/${args.id}/duplicate`));
+
+ case "export_bot":
+ return ok(
+ await client.get(`/bot/${args.id}/export`, {
+ botVersion: args.botVersion,
+ })
+ );
+
+ case "publish_bot":
+ return ok(await client.post(`/bot/${args.id}/publish`));
+
+ case "save_bot": {
+ const body: SaveBotInput = { botSteps: args.botSteps };
+ return ok(await client.post(`/bot/${args.id}/save`, body));
+ }
+
+ case "save_bot_tools":
+ return ok(
+ await client.post(
+ `/bot/${args.id}/saveTools`,
+ args.tools as ToolInputDto[]
+ )
+ );
+
+ case "get_bot_steps":
+ return ok(
+ await client.get(`/bot/${args.id}/steps`, {
+ botVersion: args.botVersion,
+ })
+ );
+
+ case "get_node_descriptors":
+ return ok(await client.get("/bot/nodeDescriptors"));
+
+ case "update_bot_version": {
+ const body: UpdateVersionInput = {};
+ if (args.name !== undefined) body.name = args.name as string;
+ if (args.importKdl !== undefined) body.importKdl = args.importKdl as string;
+ return ok(
+ await client.put(`/bot/${args.id}/version/${args.version}`, body)
+ );
+ }
+
+ case "get_bot_templates":
+ return ok(await client.get("/botTemplates"));
+
+ case "attach_source_to_bot": {
+ const body: Record = {};
+ if (args.enabled !== undefined) body.enabled = args.enabled;
+ if (args.channels !== undefined) body.channels = args.channels;
+ if (args.personaNameOverride !== undefined)
+ body.personaNameOverride = args.personaNameOverride;
+ return ok(
+ await client.post(`/bot/${args.id}/source/${args.sourceId}`, body)
+ );
+ }
+
+ case "detach_source_from_bot":
+ return ok(
+ await client.delete(`/bot/${args.id}/source/${args.sourceId}`)
+ );
+
+ case "get_bbb_templates":
+ return ok(await client.get("/bot/bbb/templates"));
+
+ default:
+ return err(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/tools/bot-testing.ts b/src/tools/bot-testing.ts
new file mode 100644
index 0000000..8a25042
--- /dev/null
+++ b/src/tools/bot-testing.ts
@@ -0,0 +1,166 @@
+import { CloseBotClient, ok, err } from "../client.js";
+import type { ToolDefinition, ToolResult, TestSession, TestSessionMessageInput, UpdateSessionInput, UpdateSessionDto, BotTestingRollbackInput, BotTestingRollbackOutput, ListLeadDto } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "create_test_session",
+ description: "Create a new test session for a bot. Returns a lead ID and source ID for the test.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "The bot ID" },
+ },
+ required: ["botId"],
+ },
+ },
+ {
+ name: "list_test_sessions",
+ description: "List test sessions for a bot with pagination",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "The bot ID" },
+ offset: { type: "number", description: "Offset (default 0)" },
+ maxCount: { type: "number", description: "Max results (default 100)" },
+ },
+ required: ["botId"],
+ },
+ },
+ {
+ name: "delete_test_session",
+ description: "Delete a test session",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "The bot ID" },
+ leadId: { type: "string", description: "The test session lead ID" },
+ },
+ required: ["botId", "leadId"],
+ },
+ },
+ {
+ name: "update_test_session",
+ description: "Update a test session (e.g., change mimic source)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "The bot ID" },
+ leadId: { type: "string", description: "The test session lead ID" },
+ mimicSourceId: { type: "string", description: "Source ID to mimic" },
+ },
+ required: ["botId", "leadId"],
+ },
+ },
+ {
+ name: "send_test_message",
+ description: "Send a message to a test session and get bot response",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "The bot ID" },
+ leadId: { type: "string", description: "The test session lead ID" },
+ message: { type: "string", description: "The message to send" },
+ },
+ required: ["botId", "leadId", "message"],
+ },
+ },
+ {
+ name: "force_test_step",
+ description: "Force the bot to process the next step in a test session",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "The bot ID" },
+ leadId: { type: "string", description: "The test session lead ID" },
+ },
+ required: ["botId", "leadId"],
+ },
+ },
+ {
+ name: "rollback_test_session",
+ description: "Rollback a test session to a specific message (deletes that message and all subsequent)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "The bot ID" },
+ leadId: { type: "string", description: "The test session lead ID" },
+ messageId: { type: "string", description: "The message ID to rollback to (this and subsequent are deleted)" },
+ },
+ required: ["botId", "leadId", "messageId"],
+ },
+ },
+];
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ switch (name) {
+ case "create_test_session":
+ return ok(
+ await client.post(`/bot/${args.botId}/testSession`)
+ );
+
+ case "list_test_sessions":
+ return ok(
+ await client.get(`/bot/${args.botId}/testSession`, {
+ offset: args.offset,
+ maxCount: args.maxCount,
+ })
+ );
+
+ case "delete_test_session":
+ return ok(
+ await client.delete(`/bot/${args.botId}/testSession/${args.leadId}`)
+ );
+
+ case "update_test_session": {
+ const body: UpdateSessionInput = {
+ mimicSourceId: args.mimicSourceId as string | undefined,
+ };
+ return ok(
+ await client.put(
+ `/bot/${args.botId}/testSession/${args.leadId}`,
+ body
+ )
+ );
+ }
+
+ case "send_test_message": {
+ const body: TestSessionMessageInput = {
+ leadId: args.leadId as string,
+ message: args.message as string,
+ };
+ return ok(
+ await client.post(`/bot/${args.botId}/testSession/message`, body)
+ );
+ }
+
+ case "force_test_step":
+ return ok(
+ await client.post(
+ `/bot/${args.botId}/testSession/${args.leadId}/force-step`
+ )
+ );
+
+ case "rollback_test_session": {
+ const body: BotTestingRollbackInput = {
+ messageId: args.messageId as string,
+ };
+ return ok(
+ await client.post(
+ `/bot/${args.botId}/testSession/${args.leadId}/rollback`,
+ body
+ )
+ );
+ }
+
+ default:
+ return err(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/tools/configuration.ts b/src/tools/configuration.ts
new file mode 100644
index 0000000..860705f
--- /dev/null
+++ b/src/tools/configuration.ts
@@ -0,0 +1,539 @@
+import { CloseBotClient, ok, err } from "../client.js";
+import type {
+ ToolDefinition, ToolResult, PersonaDto, CreatePersonaInput, UpdatePersonaInput,
+ SmartFAQDto, CreateSmartFAQRequest, AnswerMultipleFAQsRequest, AnsweredFAQFollowUpRequest,
+ ListHierarchyResult, AddHierarchyInput, HierarchyInput,
+ NotificationDto, NotificationUpdateDto, NotificationForwardingDto,
+ ApiKeyDTO, CreateApiKeyInput, CreateApiKeyOutput,
+ LiveDemoDto, LiveDemoCreateDto,
+ BotVariableDto, BotVariableUpdateInput,
+} from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ // --- Personas ---
+ {
+ name: "list_personas",
+ description: "List all personas in the agency",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "get_persona",
+ description: "Get a persona by ID",
+ inputSchema: {
+ type: "object",
+ properties: { id: { type: "string", description: "Persona ID" } },
+ required: ["id"],
+ },
+ },
+ {
+ name: "create_persona",
+ description: "Create a new persona with voice style, response settings, and AI preferences",
+ inputSchema: {
+ type: "object",
+ properties: {
+ personaName: { type: "string", description: "Persona name" },
+ description: { type: "string", description: "Description" },
+ voiceStyles: { type: "string", description: "Voice styles" },
+ howToRespond: { type: "string", description: "How to respond instructions" },
+ typoPercent: { type: "number", description: "Typo percentage (0-100)" },
+ breakupLargeMessagePercent: { type: "number", description: "Break up large message percent (0-100)" },
+ responseTime: { type: "string", description: "Response time setting" },
+ responseDelay: { type: "number", description: "Response delay in ms" },
+ aiProviderPreferences: { type: "array", items: { type: "string" }, description: "AI provider preferences" },
+ color: { type: "string", description: "Persona color" },
+ },
+ required: ["personaName"],
+ },
+ },
+ {
+ name: "update_persona",
+ description: "Update a persona. Only non-null fields are updated.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "Persona ID" },
+ personaName: { type: "string" },
+ description: { type: "string" },
+ voiceStyles: { type: "string" },
+ howToRespond: { type: "string" },
+ typoPercent: { type: "number" },
+ breakupLargeMessagePercent: { type: "number" },
+ responseTime: { type: "string" },
+ responseDelay: { type: "number" },
+ aiProviderPreferences: { type: "array", items: { type: "string" } },
+ folderId: { type: "string" },
+ favorited: { type: "boolean" },
+ trash: { type: "boolean" },
+ default: { type: "boolean" },
+ color: { type: "string" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "delete_persona",
+ description: "Delete a persona",
+ inputSchema: {
+ type: "object",
+ properties: { id: { type: "string", description: "Persona ID" } },
+ required: ["id"],
+ },
+ },
+ // --- FAQs ---
+ {
+ name: "list_faqs",
+ description: "List smart FAQs, optionally filtered by source, state, and answered status",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "Source ID" },
+ state: { type: "string", description: "State: compressed or uncompressed" },
+ answered: { type: "string", description: "Filter: answered or unanswered" },
+ },
+ },
+ },
+ {
+ name: "create_faq",
+ description: "Create a new FAQ for a source",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "Source ID" },
+ question: { type: "string", description: "FAQ question" },
+ answer: { type: "string", description: "FAQ answer" },
+ },
+ required: ["sourceId", "question"],
+ },
+ },
+ {
+ name: "delete_faq",
+ description: "Delete an FAQ",
+ inputSchema: {
+ type: "object",
+ properties: { id: { type: "string", description: "FAQ ID" } },
+ required: ["id"],
+ },
+ },
+ {
+ name: "answer_faqs",
+ description: "Answer multiple FAQs at once",
+ inputSchema: {
+ type: "object",
+ properties: {
+ faqs: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "FAQ ID" },
+ answer: { type: "string", description: "Answer text" },
+ },
+ },
+ description: "Array of FAQ answers",
+ },
+ },
+ required: ["faqs"],
+ },
+ },
+ {
+ name: "followup_answered_faqs",
+ description: "Follow up with leads who asked a now-answered FAQ",
+ inputSchema: {
+ type: "object",
+ properties: {
+ faqId: { type: "string", description: "The answered FAQ ID" },
+ leadIds: { type: "array", items: { type: "string" }, description: "Lead IDs to follow up with" },
+ },
+ required: ["faqId", "leadIds"],
+ },
+ },
+ // --- Folders ---
+ {
+ name: "list_folders",
+ description: "List all folders (hierarchy) in the agency",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "get_folder",
+ description: "Get a folder by ID",
+ inputSchema: {
+ type: "object",
+ properties: { id: { type: "string", description: "Folder ID" } },
+ required: ["id"],
+ },
+ },
+ {
+ name: "create_folder",
+ description: "Create a new folder",
+ inputSchema: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "Folder name" },
+ parentId: { type: "string", description: "Parent folder ID" },
+ },
+ required: ["name"],
+ },
+ },
+ {
+ name: "update_folder",
+ description: "Rename a folder",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "Folder ID" },
+ name: { type: "string", description: "New name" },
+ },
+ required: ["id", "name"],
+ },
+ },
+ {
+ name: "delete_folder",
+ description: "Delete a folder",
+ inputSchema: {
+ type: "object",
+ properties: { id: { type: "string", description: "Folder ID" } },
+ required: ["id"],
+ },
+ },
+ // --- Notifications ---
+ {
+ name: "list_notifications",
+ description: "List all notifications",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "update_notification",
+ description: "Update a notification (mark as viewed)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "Notification ID" },
+ viewed: { type: "boolean", description: "Mark as viewed" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "delete_notification",
+ description: "Delete a notification",
+ inputSchema: {
+ type: "object",
+ properties: { id: { type: "string", description: "Notification ID" } },
+ required: ["id"],
+ },
+ },
+ {
+ name: "get_notification_forwarding",
+ description: "Get notification forwarding settings",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "update_notification_forwarding",
+ description: "Update notification forwarding settings",
+ inputSchema: {
+ type: "object",
+ properties: {
+ enabled: { type: "boolean", description: "Enable forwarding" },
+ channelsEnabled: { type: "array", items: { type: "string" }, description: "Channels to forward" },
+ webhookEndpoint: { type: "string", description: "Webhook endpoint" },
+ },
+ },
+ },
+ // --- API Keys ---
+ {
+ name: "list_api_keys",
+ description: "List all API keys",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "create_api_key",
+ description: "Create a new API key",
+ inputSchema: {
+ type: "object",
+ properties: { name: { type: "string", description: "Key name" } },
+ required: ["name"],
+ },
+ },
+ {
+ name: "delete_api_key",
+ description: "Delete an API key",
+ inputSchema: {
+ type: "object",
+ properties: { keyId: { type: "string", description: "Key ID" } },
+ required: ["keyId"],
+ },
+ },
+ // --- Webhook ---
+ {
+ name: "send_webhook_event",
+ description: "Send a webhook event to a WebHook source",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "The WebHook source ID" },
+ body: { type: "object", description: "The webhook event body (any JSON)" },
+ },
+ required: ["sourceId", "body"],
+ },
+ },
+ // --- Live Demo ---
+ {
+ name: "list_live_demos",
+ description: "List live demos for a bot",
+ inputSchema: {
+ type: "object",
+ properties: { botId: { type: "string", description: "Bot ID" } },
+ required: ["botId"],
+ },
+ },
+ {
+ name: "create_live_demo",
+ description: "Create a live demo for a bot",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "Bot ID" },
+ name: { type: "string", description: "Demo name" },
+ mimicSourceId: { type: "string", description: "Source ID to mimic" },
+ active: { type: "boolean", description: "Whether demo is active" },
+ },
+ required: ["botId", "name"],
+ },
+ },
+ {
+ name: "update_live_demo",
+ description: "Update a live demo",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "Bot ID" },
+ key: { type: "string", description: "Demo key" },
+ name: { type: "string", description: "Demo name" },
+ mimicSourceId: { type: "string", description: "Source ID to mimic" },
+ active: { type: "boolean", description: "Whether active" },
+ },
+ required: ["botId", "key"],
+ },
+ },
+ {
+ name: "delete_live_demo",
+ description: "Delete a live demo",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "Bot ID" },
+ key: { type: "string", description: "Demo key" },
+ },
+ required: ["botId", "key"],
+ },
+ },
+ // --- Bot Source Variables ---
+ {
+ name: "get_source_variables",
+ description: "Get source variables for a bot-source pair",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "Bot ID" },
+ sourceId: { type: "string", description: "Source ID" },
+ },
+ required: ["botId", "sourceId"],
+ },
+ },
+ {
+ name: "update_source_variables",
+ description: "Update source variables for a bot-source pair",
+ inputSchema: {
+ type: "object",
+ properties: {
+ botId: { type: "string", description: "Bot ID" },
+ sourceId: { type: "string", description: "Source ID" },
+ variables: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "Variable ID" },
+ value: { type: "string", description: "Variable value" },
+ },
+ },
+ description: "Variables to update",
+ },
+ },
+ required: ["botId", "sourceId", "variables"],
+ },
+ },
+];
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ switch (name) {
+ // Personas
+ case "list_personas":
+ return ok(await client.get("/persona"));
+ case "get_persona":
+ return ok(await client.get(`/persona/${args.id}`));
+ case "create_persona": {
+ const body: CreatePersonaInput = {
+ personaName: args.personaName as string,
+ description: args.description as string | undefined,
+ voiceStyles: args.voiceStyles as string | undefined,
+ howToRespond: args.howToRespond as string | undefined,
+ typoPercent: (args.typoPercent as number) || 0,
+ breakupLargeMessagePercent: (args.breakupLargeMessagePercent as number) || 0,
+ responseTime: args.responseTime as string | undefined,
+ responseDelay: (args.responseDelay as number) || 0,
+ aiProviderPreferences: args.aiProviderPreferences as string[] | undefined,
+ color: args.color as string | undefined,
+ };
+ return ok(await client.post("/persona", body));
+ }
+ case "update_persona": {
+ const { id, ...rest } = args;
+ const body: UpdatePersonaInput = {};
+ for (const [k, v] of Object.entries(rest)) {
+ if (v !== undefined) (body as Record)[k] = v;
+ }
+ return ok(await client.put(`/persona/${id}`, body));
+ }
+ case "delete_persona":
+ return ok(await client.delete(`/persona/${args.id}`));
+
+ // FAQs
+ case "list_faqs":
+ return ok(
+ await client.get("/smart-faq", {
+ sourceId: args.sourceId,
+ state: args.state,
+ answered: args.answered,
+ })
+ );
+ case "create_faq": {
+ const body: CreateSmartFAQRequest = {
+ sourceId: args.sourceId as string,
+ question: args.question as string,
+ answer: args.answer as string | undefined,
+ };
+ return ok(await client.post("/smart-faq", body));
+ }
+ case "delete_faq":
+ return ok(await client.delete(`/smart-faq/${args.id}`));
+ case "answer_faqs": {
+ const body: AnswerMultipleFAQsRequest = {
+ faQs: args.faqs as Array<{ id?: string; answer?: string }>,
+ };
+ return ok(await client.post("/smart-faq/answer", body));
+ }
+ case "followup_answered_faqs": {
+ const body: AnsweredFAQFollowUpRequest = {
+ faqId: args.faqId as string,
+ leadIds: args.leadIds as string[],
+ };
+ return ok(await client.post("/smart-faq/answered-followup", body));
+ }
+
+ // Folders
+ case "list_folders":
+ return ok(await client.get("/hierarchy"));
+ case "get_folder":
+ return ok(await client.get(`/hierarchy/${args.id}`));
+ case "create_folder": {
+ const body: AddHierarchyInput = {
+ name: args.name as string,
+ parentId: args.parentId as string | undefined,
+ };
+ return ok(await client.post("/hierarchy", body));
+ }
+ case "update_folder": {
+ const body: HierarchyInput = { name: args.name as string };
+ return ok(await client.put(`/hierarchy/${args.id}`, body));
+ }
+ case "delete_folder":
+ return ok(await client.delete(`/hierarchy/${args.id}`));
+
+ // Notifications
+ case "list_notifications":
+ return ok(await client.get("/notifications"));
+ case "update_notification": {
+ const body: NotificationUpdateDto = { viewed: args.viewed as boolean };
+ return ok(await client.put(`/notifications/${args.id}`, body));
+ }
+ case "delete_notification":
+ return ok(await client.delete(`/notifications/${args.id}`));
+ case "get_notification_forwarding":
+ return ok(await client.get("/notifications/forwarding"));
+ case "update_notification_forwarding": {
+ const body: NotificationForwardingDto = {
+ enabled: args.enabled as boolean,
+ channelsEnabled: args.channelsEnabled as string[] | undefined,
+ webhookEndpoint: args.webhookEndpoint as string | undefined,
+ };
+ return ok(await client.put("/notifications/forwarding", body));
+ }
+
+ // API Keys
+ case "list_api_keys":
+ return ok(await client.get("/account/apiKey"));
+ case "create_api_key": {
+ const body: CreateApiKeyInput = { name: args.name as string };
+ return ok(await client.post("/account/apiKey", body));
+ }
+ case "delete_api_key":
+ return ok(await client.delete(`/account/apiKey/${args.keyId}`));
+
+ // Webhook
+ case "send_webhook_event":
+ return ok(
+ await client.post(`/webhook/event/${args.sourceId}`, args.body)
+ );
+
+ // Live Demo
+ case "list_live_demos":
+ return ok(await client.get(`/bot-live-demo/${args.botId}`));
+ case "create_live_demo": {
+ const body: LiveDemoCreateDto = {
+ name: args.name as string,
+ mimicSourceId: args.mimicSourceId as string | undefined,
+ active: (args.active as boolean) ?? true,
+ };
+ return ok(await client.post(`/bot-live-demo/${args.botId}`, body));
+ }
+ case "update_live_demo": {
+ const body: LiveDemoCreateDto = {
+ name: args.name as string | undefined,
+ mimicSourceId: args.mimicSourceId as string | undefined,
+ active: args.active as boolean | undefined,
+ };
+ return ok(
+ await client.put(`/bot-live-demo/${args.botId}/${args.key}`, body)
+ );
+ }
+ case "delete_live_demo":
+ return ok(await client.delete(`/bot-live-demo/${args.botId}/${args.key}`));
+
+ // Bot Source Variables
+ case "get_source_variables":
+ return ok(
+ await client.get(
+ `/botVariables/${args.botId}/${args.sourceId}`
+ )
+ );
+ case "update_source_variables":
+ return ok(
+ await client.post(
+ `/botVariables/${args.botId}/${args.sourceId}`,
+ args.variables as BotVariableUpdateInput[]
+ )
+ );
+
+ default:
+ return err(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/tools/lead-management.ts b/src/tools/lead-management.ts
new file mode 100644
index 0000000..ad1447e
--- /dev/null
+++ b/src/tools/lead-management.ts
@@ -0,0 +1,157 @@
+import { CloseBotClient, ok, err } from "../client.js";
+import type { ToolDefinition, ToolResult, LeadDto, LeadDtoPaginated, SearchQueryInput, LeadUpdateDto, InstanceUpdateDto } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "list_leads",
+ description: "List leads with pagination, optionally filtered by source or lead ID",
+ inputSchema: {
+ type: "object",
+ properties: {
+ page: { type: "number", description: "Page number (0-indexed)" },
+ pageSize: { type: "number", description: "Page size (default 20)" },
+ sourceId: { type: "string", description: "Filter by source ID" },
+ leadId: { type: "string", description: "Filter by lead ID" },
+ },
+ },
+ },
+ {
+ name: "search_leads",
+ description: "Search leads with advanced filters (source, channel, bot, persona, etc.)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ search: { type: "string", description: "Search query string" },
+ offset: { type: "number", description: "Result offset" },
+ count: { type: "number", description: "Number of results" },
+ sourceIds: { type: "array", items: { type: "string" }, description: "Filter by source IDs" },
+ channels: { type: "array", items: { type: "string" }, description: "Filter by channels" },
+ botIds: { type: "array", items: { type: "string" }, description: "Filter by bot IDs" },
+ personaIds: { type: "array", items: { type: "string" }, description: "Filter by persona IDs" },
+ minimumResponses: { type: "number", description: "Minimum number of responses" },
+ lastMessageDirection: { type: "string", description: "Filter by last message direction (inbound/outbound)" },
+ followUpScheduled: { type: "boolean", description: "Filter by follow-up scheduled status" },
+ },
+ },
+ },
+ {
+ name: "get_lead",
+ description: "Get detailed lead information by ID",
+ inputSchema: {
+ type: "object",
+ properties: {
+ leadId: { type: "string", description: "The lead ID" },
+ },
+ required: ["leadId"],
+ },
+ },
+ {
+ name: "update_lead_fields",
+ description: "Update custom fields on a lead",
+ inputSchema: {
+ type: "object",
+ properties: {
+ leadId: { type: "string", description: "The lead ID" },
+ fields: {
+ type: "array",
+ description: "Array of field updates",
+ items: {
+ type: "object",
+ properties: {
+ field: { type: "string", description: "Field name/key" },
+ value: { type: "string", description: "Field value" },
+ },
+ },
+ },
+ },
+ required: ["leadId", "fields"],
+ },
+ },
+ {
+ name: "delete_lead",
+ description: "Delete a lead by ID",
+ inputSchema: {
+ type: "object",
+ properties: {
+ leadId: { type: "string", description: "The lead ID" },
+ },
+ required: ["leadId"],
+ },
+ },
+ {
+ name: "update_lead_instance",
+ description: "Update a lead's bot instance (e.g., set follow-up time)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ leadId: { type: "string", description: "The lead ID" },
+ botId: { type: "string", description: "The bot ID" },
+ followUpTime: { type: "string", description: "Follow-up time (ISO 8601 datetime)" },
+ },
+ required: ["leadId", "botId"],
+ },
+ },
+];
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ switch (name) {
+ case "list_leads":
+ return ok(
+ await client.get("/lead", {
+ page: args.page,
+ pageSize: args.pageSize,
+ sourceId: args.sourceId,
+ leadId: args.leadId,
+ })
+ );
+
+ case "search_leads": {
+ const body: SearchQueryInput = {
+ search: args.search as string | undefined,
+ offset: args.offset as number | undefined,
+ count: args.count as number | undefined,
+ sourceIds: args.sourceIds as string[] | undefined,
+ channels: args.channels as string[] | undefined,
+ botIds: args.botIds as string[] | undefined,
+ personaIds: args.personaIds as string[] | undefined,
+ minimumResponses: args.minimumResponses as number | undefined,
+ lastMessageDirection: args.lastMessageDirection as string | undefined,
+ followUpScheduled: args.followUpScheduled as boolean | undefined,
+ };
+ return ok(await client.post("/lead/search", body));
+ }
+
+ case "get_lead":
+ return ok(await client.get(`/lead/${args.leadId}`));
+
+ case "update_lead_fields": {
+ const body: LeadUpdateDto = {
+ fields: args.fields as Array<{ field?: string; value?: string }>,
+ };
+ return ok(await client.put(`/lead/${args.leadId}`, body));
+ }
+
+ case "delete_lead":
+ return ok(await client.delete(`/lead/${args.leadId}`));
+
+ case "update_lead_instance": {
+ const body: InstanceUpdateDto = {};
+ if (args.followUpTime !== undefined)
+ body.followUpTime = args.followUpTime as string;
+ return ok(
+ await client.put(`/lead/${args.leadId}/instance/${args.botId}`, body)
+ );
+ }
+
+ default:
+ return err(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/tools/library.ts b/src/tools/library.ts
new file mode 100644
index 0000000..053766e
--- /dev/null
+++ b/src/tools/library.ts
@@ -0,0 +1,232 @@
+import { CloseBotClient, ok, err } from "../client.js";
+import type { ToolDefinition, ToolResult, FileDto, WebscrapePageDto, WebscrapePageUpdateDto, UploadWebscrapeToSourceInput } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "list_files",
+ description: "List all files in the knowledge base library",
+ inputSchema: { type: "object", properties: {} },
+ },
+ {
+ name: "get_file",
+ description: "Get metadata for a specific file",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "The file ID" },
+ },
+ required: ["fileId"],
+ },
+ },
+ {
+ name: "delete_file",
+ description: "Delete a file from the library",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "The file ID" },
+ },
+ required: ["fileId"],
+ },
+ },
+ {
+ name: "view_file_content",
+ description: "View the content of a file",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "The file ID" },
+ },
+ required: ["fileId"],
+ },
+ },
+ {
+ name: "get_scrape_pages",
+ description: "Get the web scrape pages for a file",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "The file ID" },
+ },
+ required: ["fileId"],
+ },
+ },
+ {
+ name: "update_scrape_pages",
+ description: "Update the web scrape pages for a file (enable/disable specific URLs)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "The file ID" },
+ pages: {
+ type: "array",
+ description: "Array of page configs",
+ items: {
+ type: "object",
+ properties: {
+ url: { type: "string", description: "Page URL" },
+ enabled: { type: "boolean", description: "Whether the page is enabled" },
+ },
+ },
+ },
+ },
+ required: ["fileId", "pages"],
+ },
+ },
+ {
+ name: "create_web_scrape",
+ description: "Create a new web scrape file from a URL",
+ inputSchema: {
+ type: "object",
+ properties: {
+ scrapeUrl: { type: "string", description: "URL to scrape" },
+ maxBreadth: { type: "number", description: "Maximum breadth of the scrape" },
+ maxDepth: { type: "number", description: "Maximum depth of the scrape" },
+ },
+ required: ["scrapeUrl"],
+ },
+ },
+ {
+ name: "attach_file_to_source",
+ description: "Attach a library file to a source for knowledge base use",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "The file ID" },
+ sourceId: { type: "string", description: "The source ID" },
+ },
+ required: ["fileId", "sourceId"],
+ },
+ },
+ {
+ name: "detach_file_from_source",
+ description: "Detach a library file from a source",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "The file ID" },
+ sourceId: { type: "string", description: "The source ID" },
+ },
+ required: ["fileId", "sourceId"],
+ },
+ },
+ {
+ name: "upload_file",
+ description: "Upload a file to the library. Provide base64-encoded file content.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileName: { type: "string", description: "File name with extension" },
+ fileContent: { type: "string", description: "Base64-encoded file content" },
+ mimeType: { type: "string", description: "MIME type of the file (e.g., text/plain, application/pdf)" },
+ },
+ required: ["fileName", "fileContent"],
+ },
+ },
+ {
+ name: "replace_file_content",
+ description: "Replace the content of an existing file. Provide base64-encoded new content.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ fileId: { type: "string", description: "The file ID" },
+ fileName: { type: "string", description: "File name with extension" },
+ fileContent: { type: "string", description: "Base64-encoded new file content" },
+ mimeType: { type: "string", description: "MIME type of the file" },
+ },
+ required: ["fileId", "fileContent"],
+ },
+ },
+];
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ switch (name) {
+ case "list_files":
+ return ok(await client.get("/library/files"));
+
+ case "get_file":
+ return ok(await client.get(`/library/files/${args.fileId}`));
+
+ case "delete_file":
+ return ok(await client.delete(`/library/files/${args.fileId}`));
+
+ case "view_file_content":
+ return ok(await client.get(`/library/files/${args.fileId}/view`));
+
+ case "get_scrape_pages":
+ return ok(
+ await client.get(
+ `/library/files/${args.fileId}/scrape-pages`
+ )
+ );
+
+ case "update_scrape_pages": {
+ const body: WebscrapePageUpdateDto = {
+ pages: args.pages as WebscrapePageDto[],
+ };
+ return ok(
+ await client.put(`/library/files/${args.fileId}/scrape-pages`, body)
+ );
+ }
+
+ case "create_web_scrape": {
+ const body: UploadWebscrapeToSourceInput = {
+ scrapeUrl: args.scrapeUrl as string,
+ maxBreadth: (args.maxBreadth as number) || 10,
+ maxDepth: (args.maxDepth as number) || 2,
+ };
+ return ok(await client.post("/library/webscrape", body));
+ }
+
+ case "attach_file_to_source":
+ return ok(
+ await client.post(
+ `/library/files/${args.fileId}/source/${args.sourceId}`
+ )
+ );
+
+ case "detach_file_from_source":
+ return ok(
+ await client.delete(
+ `/library/files/${args.fileId}/source/${args.sourceId}`
+ )
+ );
+
+ case "upload_file": {
+ const buffer = Buffer.from(args.fileContent as string, "base64");
+ const blob = new Blob([buffer], {
+ type: (args.mimeType as string) || "application/octet-stream",
+ });
+ const formData = new FormData();
+ formData.append("file", blob, args.fileName as string);
+ return ok(await client.postFormData("/library/files", formData));
+ }
+
+ case "replace_file_content": {
+ const buffer = Buffer.from(args.fileContent as string, "base64");
+ const blob = new Blob([buffer], {
+ type: (args.mimeType as string) || "application/octet-stream",
+ });
+ const formData = new FormData();
+ formData.append(
+ "newFile",
+ blob,
+ (args.fileName as string) || "file"
+ );
+ return ok(
+ await client.putFormData(`/library/files/${args.fileId}`, formData)
+ );
+ }
+
+ default:
+ return err(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/tools/source-management.ts b/src/tools/source-management.ts
new file mode 100644
index 0000000..f983399
--- /dev/null
+++ b/src/tools/source-management.ts
@@ -0,0 +1,213 @@
+import { CloseBotClient, ok, err } from "../client.js";
+import type { ToolDefinition, ToolResult, SourceDto, SourceDtoPaginated, AddSourceInput, UpdateSourceInput, SourceCalendarDto, SourceChannelDto, SourceFieldCollectionDto, SourceTagDto } from "../types.js";
+
+export const tools: ToolDefinition[] = [
+ {
+ name: "list_sources",
+ description: "List sources in the agency with pagination and filtering",
+ inputSchema: {
+ type: "object",
+ properties: {
+ page: { type: "number", description: "Page number (0-indexed)" },
+ pageSize: { type: "number", description: "Page size (max 100, default 20)" },
+ query: { type: "string", description: "Search by source name" },
+ category: { type: "string", description: "Filter by category (GHL, HubSpot, WebHook, etc.)" },
+ order: { type: "string", description: "Order results. +/- prefix for asc/desc (default: +id)" },
+ forceTokenRefresh: { type: "boolean", description: "Refresh access tokens for returned sources" },
+ },
+ },
+ },
+ {
+ name: "get_source",
+ description: "Get detailed information for a single source",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The source ID" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "add_source",
+ description: "Add a new source to the agency",
+ inputSchema: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "Source name" },
+ category: { type: "string", description: "Source category (GHL, WebHook, etc.)" },
+ key: { type: "string", description: "Source key/identifier" },
+ accessToken: { type: "string", description: "Access token for the source" },
+ refreshToken: { type: "string", description: "Refresh token" },
+ expiresIn: { type: "number", description: "Token expiry in seconds" },
+ autoShutoff: { type: "boolean", description: "Enable auto shutoff" },
+ gracefulGoodbye: { type: "boolean", description: "Enable graceful goodbye" },
+ summarizeAttachments: { type: "boolean", description: "Enable attachment summarization" },
+ webhookCallback: { type: "string", description: "Webhook callback URL" },
+ },
+ required: ["name"],
+ },
+ },
+ {
+ name: "update_source",
+ description: "Update source fields. Only non-null fields are updated.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ sourceId: { type: "string", description: "The source ID" },
+ name: { type: "string", description: "New name" },
+ autoShutoff: { type: "boolean", description: "Auto shutoff setting" },
+ gracefulGoodbye: { type: "boolean", description: "Graceful goodbye setting" },
+ isAvailabilityContactTimezone: { type: "boolean", description: "Use contact timezone for availability" },
+ summarizeAttachments: { type: "boolean", description: "Summarize attachments" },
+ respondToReactions: { type: "boolean", description: "Respond to reactions" },
+ markConversationsAsUnread: { type: "boolean", description: "Mark conversations as unread" },
+ webhookCallback: { type: "string", description: "Webhook callback URL" },
+ },
+ required: ["sourceId"],
+ },
+ },
+ {
+ name: "delete_source",
+ description: "Delete a source and all associated data. Also attempts to uninstall from external CRM.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The source ID" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "list_source_calendars",
+ description: "List all calendars attached to a source (manual + CRM calendars)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The source ID" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "list_source_channels",
+ description: "List all channels available to a source",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The source ID" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "list_source_fields",
+ description: "List available fields for a source (contact, location, custom values)",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The source ID" },
+ },
+ required: ["id"],
+ },
+ },
+ {
+ name: "list_source_tags",
+ description: "List all tags for a source",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "string", description: "The source ID" },
+ },
+ required: ["id"],
+ },
+ },
+];
+
+export async function handler(
+ client: CloseBotClient,
+ name: string,
+ args: Record
+): Promise {
+ try {
+ switch (name) {
+ case "list_sources":
+ return ok(
+ await client.get("/agency/source", {
+ page: args.page,
+ pageSize: args.pageSize,
+ query: args.query,
+ category: args.category,
+ order: args.order,
+ forceTokenRefresh: args.forceTokenRefresh,
+ })
+ );
+
+ case "get_source":
+ return ok(await client.get(`/agency/source/${args.id}`));
+
+ case "add_source": {
+ const body: AddSourceInput = {
+ name: args.name as string,
+ category: args.category as string | undefined,
+ key: args.key as string | undefined,
+ accessToken: args.accessToken as string | undefined,
+ refreshToken: args.refreshToken as string | undefined,
+ expiresIn: args.expiresIn as number | undefined,
+ autoShutoff: args.autoShutoff as boolean | undefined,
+ gracefulGoodbye: args.gracefulGoodbye as boolean | undefined,
+ summarizeAttachments: args.summarizeAttachments as boolean | undefined,
+ webhookCallback: args.webhookCallback as string | undefined,
+ };
+ return ok(await client.post("/agency/source", body));
+ }
+
+ case "update_source": {
+ const { sourceId, ...rest } = args;
+ const body: UpdateSourceInput = {};
+ if (rest.name !== undefined) body.name = rest.name as string;
+ if (rest.autoShutoff !== undefined) body.autoShutoff = rest.autoShutoff as boolean;
+ if (rest.gracefulGoodbye !== undefined) body.gracefulGoodbye = rest.gracefulGoodbye as boolean;
+ if (rest.isAvailabilityContactTimezone !== undefined)
+ body.isAvailabilityContactTimezone = rest.isAvailabilityContactTimezone as boolean;
+ if (rest.summarizeAttachments !== undefined)
+ body.summarizeAttachments = rest.summarizeAttachments as boolean;
+ if (rest.respondToReactions !== undefined)
+ body.respondToReactions = rest.respondToReactions as boolean;
+ if (rest.markConversationsAsUnread !== undefined)
+ body.markConversationsAsUnread = rest.markConversationsAsUnread as boolean;
+ if (rest.webhookCallback !== undefined)
+ body.webhookCallback = rest.webhookCallback as string;
+ return ok(await client.put(`/agency/source/${sourceId}`, body));
+ }
+
+ case "delete_source":
+ return ok(await client.delete(`/agency/source/${args.id}`));
+
+ case "list_source_calendars":
+ return ok(
+ await client.get(`/agency/source/${args.id}/calendars`)
+ );
+
+ case "list_source_channels":
+ return ok(
+ await client.get(`/agency/source/${args.id}/channels`)
+ );
+
+ case "list_source_fields":
+ return ok(
+ await client.get(`/agency/source/${args.id}/fields`)
+ );
+
+ case "list_source_tags":
+ return ok(
+ await client.get(`/agency/source/${args.id}/tags`)
+ );
+
+ default:
+ return err(`Unknown tool: ${name}`);
+ }
+ } catch (error) {
+ return err(error);
+ }
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..5dd497c
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,940 @@
+// ============================================================================
+// CloseBot API Types — Generated from OpenAPI 3.0.1 Swagger Spec
+// ============================================================================
+
+// ---------- Common / Shared ----------
+
+export interface ProblemDetails {
+ type?: string | null;
+ title?: string | null;
+ status?: number | null;
+ detail?: string | null;
+ instance?: string | null;
+ [key: string]: unknown;
+}
+
+// ---------- Account ----------
+
+export interface CreateApiKeyInput {
+ name?: string | null;
+}
+
+export interface CreateApiKeyOutput {
+ id?: string | null;
+ key?: string | null;
+ name?: string | null;
+ createdAt?: string;
+ expiresAt?: string;
+}
+
+export interface ApiKeyDTO {
+ id?: string | null;
+ name?: string | null;
+ createdAt?: string;
+ expiresAt?: string;
+}
+
+// ---------- Agency ----------
+
+export interface AgencyDto {
+ id?: string | null;
+ name?: string | null;
+ members?: AgencyMemberDto[] | null;
+}
+
+export interface AgencyMemberDto {
+ accountId?: string | null;
+ role?: string | null;
+ authId?: string | null;
+ name?: string | null;
+ email?: string | null;
+ status?: string | null;
+}
+
+export interface InviteUserInput {
+ invites?: Invite[] | null;
+}
+
+export interface Invite {
+ email?: string | null;
+ role?: string | null;
+ sourceIds?: string[] | null;
+}
+
+// ---------- Billing ----------
+
+export interface BalanceDto {
+ balance?: number;
+ currency?: string | null;
+}
+
+export interface BillingOptionsDto {
+ overBillingEnabled?: boolean;
+ usageBillingEnabled?: boolean;
+ autoRefillEnabled?: boolean;
+ topUpAmount?: number;
+ refillThreshold?: number;
+ currency?: string | null;
+}
+
+export interface UpdateBillingConfigInput {
+ overBillingEnabled?: boolean | null;
+ autoRefillEnabled?: boolean | null;
+ topUpAmount?: number | null;
+ refillThreshold?: number | null;
+}
+
+export interface CreateRefillDto {
+ amount?: number;
+ currency?: string | null;
+}
+
+export interface ReBillingDto {
+ reBillingConfigured?: boolean;
+ responseUnitCost?: string | null;
+ storageUnitCost?: string | null;
+ userUnitCost?: string | null;
+ tokenMultiplier?: string | null;
+}
+
+export interface ReBillingUpdateInput {
+ enabled?: boolean | null;
+ responseCost?: string | null;
+ storageCost?: string | null;
+ userCost?: string | null;
+ tokenMultiplier?: string | null;
+}
+
+export interface TransactionDto {
+ id?: number;
+ status?: string | null;
+ description?: string | null;
+ amount?: number;
+ currency?: string | null;
+ createdAt?: string;
+ receiptUrl?: string | null;
+ usage?: BilledUsageDto;
+}
+
+export interface AddSourceTransactionDto {
+ amount?: string | null;
+ description?: string | null;
+}
+
+export interface BilledUsageDto {
+ startTime?: string;
+ endTime?: string;
+ responses?: number;
+ libraryBytes?: number;
+ users?: number;
+ responseCost?: number;
+ libraryCost?: number;
+ userCost?: number;
+}
+
+// ---------- Bot ----------
+
+export interface BotDto {
+ id?: string | null;
+ name?: string | null;
+ modifiedAt?: string | null;
+ modifiedBy?: string | null;
+ versions?: BotVersionDto[] | null;
+ sources?: BotSourceDto[] | null;
+ personaIds?: string[] | null;
+ favorited?: boolean;
+ locked?: boolean;
+ reschedulingEnabled?: boolean;
+ category?: string | null;
+ folderId?: string | null;
+ followUpActive?: boolean;
+ followUpSequences?: FollowUpSequenceDto[] | null;
+ smartFollowUp?: boolean;
+ followUpRepeat?: boolean;
+ followUpVarianceMinutes?: number;
+ followUpExtraPrompt?: string | null;
+ tools?: BotToolDto[] | null;
+}
+
+export interface BotVersionDto {
+ version?: string | null;
+ name?: string | null;
+ published?: boolean;
+ modifiedAt?: string;
+ modifiedBy?: string | null;
+}
+
+export interface BotSourceDto {
+ id?: string | null;
+ category?: string | null;
+ key?: string | null;
+ name?: string | null;
+ tags?: ContactTag[] | null;
+ channelList?: string[] | null;
+ personaNameOverride?: string | null;
+ enabled?: boolean;
+}
+
+export interface ContactTag {
+ name?: string | null;
+ approveDeny?: boolean;
+ id?: string | null;
+}
+
+export interface FollowUpSequenceDto {
+ order?: number;
+ duration?: number;
+ unit?: string | null;
+}
+
+export interface BotToolDto {
+ id?: string | null;
+ type?: string | null;
+ enabled?: boolean;
+ options?: Record;
+}
+
+export interface CreateBotInput {
+ name?: string | null;
+ templateId?: string | null;
+ importKdl?: string | null;
+ folderId?: string | null;
+ category?: string | null;
+}
+
+export interface AiCreateBotInput {
+ name?: string | null;
+ description?: string | null;
+ category?: string | null;
+ folderId?: string | null;
+}
+
+export interface UpdateBotInput {
+ favorite?: boolean | null;
+ trash?: boolean | null;
+ locked?: boolean | null;
+ rescheduling?: boolean | null;
+ name?: string | null;
+ folderId?: string | null;
+ category?: string | null;
+ followUpActive?: boolean | null;
+ followUpSequences?: FollowUpSequenceDto[] | null;
+ smartFollowUp?: boolean | null;
+ followUpRepeat?: boolean | null;
+ followUpVarianceMinutes?: number | null;
+ followUpExtraPrompt?: string | null;
+}
+
+export interface UpdateBotErrorResponse {
+ message?: string | null;
+}
+
+export interface AttachSourceInput {
+ tags?: ContactTag[] | null;
+ channels?: string[] | null;
+ personaNameOverride?: string | null;
+ enabled?: boolean | null;
+}
+
+export interface ExportBotResponse {
+ id?: string | null;
+ kdl?: string | null;
+ version?: string | null;
+}
+
+export interface SaveBotInput {
+ botSteps?: unknown;
+}
+
+export interface SaveBotResponse {
+ version?: string | null;
+ invalidPaths?: string[] | null;
+ message?: string | null;
+}
+
+export interface PublishBotResponse {
+ version?: string | null;
+ message?: unknown;
+}
+
+export interface UpdateVersionInput {
+ name?: string | null;
+ importKdl?: string | null;
+}
+
+export interface ToolInputDto {
+ type?: string | null;
+ enabled?: boolean;
+ options?: unknown;
+}
+
+// ---------- Bot Metrics ----------
+
+export interface AgencyDashboardSummaryResponse {
+ currentMonthMessageCount?: number;
+ lastMonthMessageCount?: number;
+ totalStorage?: number;
+ currentMonthSuccessfulBookings?: number;
+ lastMonthSuccessfulBookings?: number;
+ currentMonthActiveSources?: number;
+ lastMonthActiveSources?: number;
+ currentUsers?: number;
+ currentMonthContacts?: number;
+ lastMonthContacts?: number;
+}
+
+export interface BotMetricAction {
+ timestamp?: string;
+ actionId?: string | null;
+ leadId?: string | null;
+ sourceId?: string | null;
+ botId?: string | null;
+ nodeId?: number;
+ frontendNodeId?: string | null;
+}
+
+export interface BotMetricMessage {
+ messageId?: string | null;
+ sourceId?: string | null;
+ leadId?: string | null;
+ botId?: string | null;
+ personaId?: string | null;
+ channel?: string | null;
+ fromBot?: boolean;
+ direction?: string | null;
+ message?: string | null;
+ attachments?: BotMetricAttachment[] | null;
+ timestamp?: string;
+ activities?: BotMetricActivity[] | null;
+}
+
+export interface BotMetricAttachment {
+ url?: string | null;
+}
+
+export interface BotMetricActivity {
+ activity?: string | null;
+ data?: string | null;
+ timeData?: string | null;
+}
+
+export interface BotMetricLog {
+ timestamp?: string;
+ botId?: string | null;
+ messageId?: string | null;
+ sourceId?: string | null;
+ leadId?: string | null;
+ actionId?: string | null;
+ severity?: number | null;
+ message?: string | null;
+ prompt?: BotMetricPrompt[] | null;
+ response?: string | null;
+ promptTokens?: number | null;
+ completionTokens?: number | null;
+ provider?: string | null;
+ model?: string | null;
+ purpose?: string | null;
+ body?: string | null;
+}
+
+export interface BotMetricPrompt {
+ kind?: string | null;
+ body?: string | null;
+}
+
+export interface LeaderboardResponse {
+ agencyName?: string | null;
+ agencyId?: string | null;
+ count?: number;
+ value?: number;
+ rank?: number;
+ isCurrentAgency?: boolean;
+}
+
+export interface MessageFeedbackRequest {
+ messageId?: string | null;
+ leadId?: string | null;
+ reasons?: string | null;
+ liked?: boolean | null;
+}
+
+export interface MessageFeedbackResponse {
+ feedbackMessageIds?: string[] | null;
+}
+
+export interface MessageLikesResponse {
+ likedMessageIds?: string[] | null;
+}
+
+export interface MessageReason {
+ reason?: string | null;
+}
+
+// ---------- Bot Source Variables ----------
+
+export interface BotVariableDto {
+ id?: string | null;
+ name?: string | null;
+ value?: string | null;
+}
+
+export interface BotVariableUpdateInput {
+ id?: string | null;
+ value?: string | null;
+}
+
+// ---------- Bot Templates ----------
+
+export interface BotTemplateDto {
+ id?: string | null;
+ name?: string | null;
+ description?: string | null;
+ level?: string | null;
+ industry?: string | null;
+ tags?: string[] | null;
+ videoUrl?: string | null;
+ previewUrl?: string | null;
+ tier?: string | null;
+}
+
+// ---------- Bot Testing ----------
+
+export interface TestSession {
+ leadId?: string | null;
+ sourceId?: string | null;
+}
+
+export interface TestSessionMessageInput {
+ leadId?: string | null;
+ message?: string | null;
+}
+
+export interface UpdateSessionInput {
+ mimicSourceId?: string | null;
+}
+
+export interface UpdateSessionDto {
+ sessionId?: string | null;
+}
+
+export interface BotTestingRollbackInput {
+ messageId?: string | null;
+}
+
+export interface BotTestingRollbackOutput {
+ deletedMessageIds?: string[] | null;
+ deletedActionIds?: string[] | null;
+}
+
+export interface ListLeadDto {
+ leads?: LeadDto[] | null;
+ total?: number;
+}
+
+// ---------- Hierarchy (Folders) ----------
+
+export interface AddHierarchyInput {
+ name?: string | null;
+ parentId?: string | null;
+}
+
+export interface AddHierarchyOutput {
+ id?: string | null;
+}
+
+export interface ListHierarchyResult {
+ id?: string | null;
+ name?: string | null;
+ bots?: string[] | null;
+ personas?: string[] | null;
+ children?: string[] | null;
+}
+
+export interface HierarchyInput {
+ name?: string | null;
+}
+
+// ---------- Lead ----------
+
+export interface LeadDto {
+ id?: string | null;
+ name?: string | null;
+ contactId?: string | null;
+ lastMessageTime?: string | null;
+ lastMessage?: string | null;
+ lastMessageDirection?: string | null;
+ lastMessageBotId?: string | null;
+ mostRecentFailureReason?: string | null;
+ mimicSourceId?: string | null;
+ source?: LeadSourceDto;
+ tags?: string[] | null;
+ fields?: LeadFieldDto[] | null;
+ instances?: BotInstanceDto[] | null;
+}
+
+export interface LeadDtoPaginated {
+ total?: number;
+ results?: LeadDto[] | null;
+ page?: number;
+ pageSize?: number;
+}
+
+export interface LeadSourceDto {
+ id?: string | null;
+ name?: string | null;
+}
+
+export interface LeadFieldDto {
+ field?: string | null;
+ value?: string | null;
+}
+
+export interface LeadFieldUpdateDto {
+ field?: string | null;
+ value?: string | null;
+}
+
+export interface LeadUpdateDto {
+ fields?: LeadFieldUpdateDto[] | null;
+}
+
+export interface SearchQueryInput {
+ offset?: number | null;
+ count?: number | null;
+ search?: string | null;
+ sourceIds?: string[] | null;
+ channels?: string[] | null;
+ botIds?: string[] | null;
+ personaIds?: string[] | null;
+ minimumResponses?: number | null;
+ lastMessageDirection?: string | null;
+ followUpScheduled?: boolean | null;
+}
+
+export interface InstanceUpdateDto {
+ followUpTime?: string | null;
+}
+
+export interface BotInstanceDto {
+ botId?: string | null;
+ botVersion?: string | null;
+ followUpTimezoneKind?: string | null;
+ followUpTimezone?: string | null;
+ followUpTime?: string | null;
+ isSmartFollowUpTime?: boolean;
+}
+
+// ---------- Library ----------
+
+export interface FileDto {
+ fileId?: string | null;
+ fileName?: string | null;
+ lastModified?: string;
+ fileType?: string | null;
+ fileStatus?: string | null;
+ fileSize?: number;
+ estimatedFileSize?: number;
+ sources?: FileSourceDto[] | null;
+ accountId?: string | null;
+ uri?: string | null;
+}
+
+export interface FileSourceDto {
+ id?: string | null;
+ name?: string | null;
+ category?: string | null;
+}
+
+export interface UploadWebscrapeToSourceInput {
+ scrapeUrl?: string | null;
+ maxBreadth?: number;
+ maxDepth?: number;
+}
+
+export interface WebscrapePageDto {
+ url?: string | null;
+ enabled?: boolean;
+}
+
+export interface WebscrapePageUpdateDto {
+ pages?: WebscrapePageDto[] | null;
+}
+
+// ---------- Live Demo ----------
+
+export interface LiveDemoCreateDto {
+ name?: string | null;
+ mimicSourceId?: string | null;
+ active?: boolean;
+}
+
+export interface LiveDemoDto {
+ name?: string | null;
+ key?: string | null;
+ organizationId?: string | null;
+ active?: boolean;
+ mimicSourceId?: string | null;
+ sourceVariables?: BotVariableDto[] | null;
+}
+
+export interface LiveDemoSessionDto {
+ key?: string | null;
+ sessionLeadId?: string | null;
+}
+
+export interface LiveDemoMessageInput {
+ message?: string | null;
+}
+
+// ---------- Notification ----------
+
+export interface NotificationDto {
+ id?: string | null;
+ kind?: string | null;
+ title?: string | null;
+ body?: string | null;
+ viewed?: boolean;
+ timestamp?: string;
+ metadata?: NotificationMetadata;
+}
+
+export interface NotificationMetadata {
+ aiProviderId?: string | null;
+ sourceId?: string | null;
+ botId?: string | null;
+ rawAiError?: string | null;
+}
+
+export interface NotificationUpdateDto {
+ viewed?: boolean;
+}
+
+export interface NotificationForwardingDto {
+ enabled?: boolean;
+ channelsEnabled?: string[] | null;
+ webhookEndpoint?: string | null;
+}
+
+// ---------- Persona ----------
+
+export interface PersonaDto {
+ id?: string | null;
+ agencyId?: string | null;
+ personaName?: string | null;
+ description?: string | null;
+ color?: string | null;
+ imageUri?: string | null;
+ voiceStyles?: string | null;
+ howToRespond?: string | null;
+ typoPercent?: number;
+ breakupLargeMessagePercent?: number;
+ responseTime?: string | null;
+ responseDelay?: number;
+ modifiedAt?: string | null;
+ modifiedBy?: string | null;
+ aiProviderPreferences?: string[] | null;
+ folderId?: string | null;
+ botIds?: string[] | null;
+ bots?: BotPersonaDto[] | null;
+ favorited?: boolean;
+ default?: boolean;
+}
+
+export interface BotPersonaDto {
+ id?: string | null;
+ name?: string | null;
+}
+
+export interface CreatePersonaInput {
+ personaName?: string | null;
+ description?: string | null;
+ voiceStyles?: string | null;
+ howToRespond?: string | null;
+ typoPercent?: number;
+ breakupLargeMessagePercent?: number;
+ responseTime?: string | null;
+ responseDelay?: number;
+ aiProviderPreferences?: string[] | null;
+ color?: string | null;
+ imageData?: string | null;
+}
+
+export interface UpdatePersonaInput {
+ personaName?: string | null;
+ description?: string | null;
+ voiceStyles?: string | null;
+ howToRespond?: string | null;
+ typoPercent?: number | null;
+ breakupLargeMessagePercent?: number | null;
+ responseTime?: string | null;
+ responseDelay?: number | null;
+ aiProviderPreferences?: string[] | null;
+ folderId?: string | null;
+ favorited?: boolean | null;
+ trash?: boolean | null;
+ default?: boolean | null;
+ color?: string | null;
+ imageData?: string | null;
+}
+
+// ---------- Smart FAQ ----------
+
+export interface SmartFAQDto {
+ id?: string | null;
+ agencyId?: string | null;
+ sourceId?: string | null;
+ question?: string | null;
+ answer?: string | null;
+ leadIds?: string[] | null;
+ state?: number;
+}
+
+export interface CreateSmartFAQRequest {
+ sourceId?: string | null;
+ question?: string | null;
+ answer?: string | null;
+}
+
+export interface AnswerMultipleFAQsRequest {
+ faQs?: AnswerFAQRequest[] | null;
+}
+
+export interface AnswerFAQRequest {
+ id?: string | null;
+ answer?: string | null;
+}
+
+export interface AnsweredFAQFollowUpRequest {
+ faqId?: string | null;
+ leadIds?: string[] | null;
+}
+
+// ---------- Source ----------
+
+export interface SourceDto {
+ agencyId?: string | null;
+ sourceId?: string | null;
+ name?: string | null;
+ category?: string | null;
+ key?: string | null;
+ accessToken?: string | null;
+ address?: string | null;
+ connected?: boolean;
+ autoShutoff?: boolean;
+ gracefulGoodbye?: boolean;
+ bots?: SourceBotDto[] | null;
+ accountsWithAccess?: string[] | null;
+ isAvailabilityContactTimezone?: boolean;
+ respondWindows?: SourceAvailabilityDto[] | null;
+ doNotRespondWindows?: SourceDoNotRespondWindowDto[] | null;
+ summarizeAttachments?: boolean;
+ respondToReactions?: boolean;
+ markConversationAsUnread?: boolean;
+ webhookCallback?: string | null;
+ wallet?: SourceWalletDto;
+}
+
+export interface SourceDtoPaginated {
+ total?: number;
+ results?: SourceDto[] | null;
+ page?: number;
+ pageSize?: number;
+}
+
+export interface SourceBotDto {
+ id?: string | null;
+ botName?: string | null;
+ tags?: ContactTag[] | null;
+ channels?: string[] | null;
+ personaNameOverride?: string | null;
+ enabled?: boolean;
+}
+
+export interface SourceAvailabilityDto {
+ dayOfWeekUtc?: string | null;
+ startTimeUtc?: string | null;
+ duration?: string | null;
+}
+
+export interface SourceDoNotRespondWindowDto {
+ start?: string;
+ end?: string;
+}
+
+export interface SourceWalletDto {
+ reBilling?: boolean;
+ autoRefill?: boolean;
+ topUpAmount?: number;
+ refillThreshold?: number;
+ stripeCustomerId?: string | null;
+ currency?: string | null;
+ responseUnitCostOverride?: string | null;
+ storageUnitCostOverride?: string | null;
+ userUnitCostOverride?: string | null;
+}
+
+export interface AddSourceInput {
+ name?: string | null;
+ category?: string | null;
+ key?: string | null;
+ accessToken?: string | null;
+ refreshToken?: string | null;
+ expiresIn?: number | null;
+ autoShutoff?: boolean | null;
+ gracefulGoodbye?: boolean | null;
+ summarizeAttachments?: boolean | null;
+ webhookCallback?: string | null;
+}
+
+export interface UpdateSourceInput {
+ name?: string | null;
+ autoShutoff?: boolean | null;
+ gracefulGoodbye?: boolean | null;
+ isAvailabilityContactTimezone?: boolean | null;
+ respondWindows?: SourceAvailabilityDto[] | null;
+ doNotRespondWindows?: SourceDoNotRespondWindowDto[] | null;
+ summarizeAttachments?: boolean | null;
+ respondToReactions?: boolean | null;
+ markConversationsAsUnread?: boolean | null;
+ wallet?: UpdateSourceWalletInput;
+ accountsWithAccess?: string[] | null;
+ webhookCallback?: string | null;
+}
+
+export interface UpdateSourceWalletInput {
+ reBilling?: boolean | null;
+ autoRefill?: boolean | null;
+ topUpAmount?: number | null;
+ refillThreshold?: number | null;
+ responseUnitCostOverride?: string | null;
+ storageUnitCostOverride?: string | null;
+ userUnitCostOverride?: string | null;
+ stripeCustomerId?: string | null;
+}
+
+export interface SourceCalendarDto {
+ name?: string | null;
+ id?: string | null;
+}
+
+export interface SourceChannelDto {
+ name?: string | null;
+ id?: string | null;
+}
+
+export interface SourceFieldCollectionDto {
+ contact?: SourceFieldDto[] | null;
+ location?: SourceFieldDto[] | null;
+ customValue?: SourceFieldDto[] | null;
+}
+
+export interface SourceFieldDto {
+ name?: string | null;
+ fieldKey?: string | null;
+ dataType?: string | null;
+}
+
+export interface SourceTagDto {
+ name?: string | null;
+ id?: string | null;
+}
+
+// ---------- Node Descriptors ----------
+
+export interface NodeInformation {
+ dataTypes?: DataTypeInfo[] | null;
+ atomicNodes?: NodeInfo[] | null;
+ groups?: BotNodeGroup[] | null;
+ tools?: ToolInfo[] | null;
+}
+
+export interface DataTypeInfo {
+ dataTypeName?: string | null;
+ properties?: PropertyInfo[] | null;
+ displayName?: string | null;
+}
+
+export interface NodeInfo {
+ className?: string | null;
+ displayName?: string | null;
+ description?: string | null;
+ group?: string | null;
+ properties?: PropertyInfo[] | null;
+ outputs?: OutputInfo[] | null;
+ outputHandles?: OutputHandleInfo[] | null;
+ dynamicHandles?: DynamicOutputHandleInfo[] | null;
+ hasInputHandle?: boolean;
+ hasDynamicVariables?: boolean;
+ helpUrl?: string | null;
+ requiresPaid?: boolean;
+ order?: number;
+ hidden?: boolean;
+}
+
+export interface PropertyInfo {
+ name?: string | null;
+ type?: string | null;
+ enumValues?: string[] | null;
+ displayName?: string | null;
+ defaultValue?: string | null;
+ group?: string | null;
+ conditions?: BotNodePropertyCondition[] | null;
+}
+
+export interface BotNodePropertyCondition {
+ key?: string | null;
+ value?: string | null;
+}
+
+export interface OutputInfo {
+ name?: string | null;
+ displayName?: string | null;
+}
+
+export interface OutputHandleInfo {
+ name?: string | null;
+ label?: string | null;
+ color?: string | null;
+}
+
+export interface DynamicOutputHandleInfo {
+ linkedProperty?: string | null;
+ labelPropertyName?: string | null;
+}
+
+export interface BotNodeGroup {
+ name?: string | null;
+ order?: number;
+}
+
+export interface ToolInfo {
+ className?: string | null;
+ displayName?: string | null;
+ description?: string | null;
+ properties?: PropertyInfo[] | null;
+ helpUrl?: string | null;
+ hidden?: boolean;
+}
+
+// ---------- MCP Tool Registration Types ----------
+
+export interface ToolDefinition {
+ name: string;
+ description: string;
+ inputSchema: {
+ type: "object";
+ properties: Record;
+ required?: string[];
+ };
+}
+
+export interface ToolGroup {
+ tools: ToolDefinition[];
+ handler: (name: string, args: Record) => Promise;
+}
+
+export interface ToolResult {
+ content: Array<{ type: "text"; text: string } | { type: "text"; text: string; [key: string]: unknown }>;
+ isError?: boolean;
+ structuredContent?: unknown;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..156b6d5
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}