feat: add CloseBot MCP server source code (119 tools, 14 modules)

This commit is contained in:
Jake Shore 2026-02-11 14:01:24 -05:00
parent 9430922cb3
commit 7fd696382a
25 changed files with 6522 additions and 0 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
CLOSEBOT_API_KEY=your_closebot_api_key_here

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
.env
*.log

7
Dockerfile Normal file
View File

@ -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"]

87
README.md Normal file
View File

@ -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

1713
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@ -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"
}
}

4
railway.json Normal file
View File

@ -0,0 +1,4 @@
{
"build": { "builder": "NIXPACKS" },
"deploy": { "startCommand": "npm start" }
}

View File

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
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 `<span style="color:${color};font-size:12px;">${sign}${pct.toFixed(1)}%</span>`;
}
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) => `
<div style="background:#1a1a2e;border-radius:12px;padding:16px;text-align:center;">
<div style="font-size:28px;margin-bottom:4px;">${c.icon}</div>
<div style="font-size:24px;font-weight:700;color:#fff;">${typeof c.value === "number" ? c.value.toLocaleString() : c.value}</div>
<div style="font-size:12px;color:#888;margin-top:4px;">${escapeHtml(c.label)}</div>
${c.change ? `<div style="margin-top:4px;">${c.change}</div>` : ""}
</div>`
)
.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<string, unknown>) => (d.count as number) || 0), 1);
const bars = bookingData
.slice(-14) // last 14 data points
.map((d: Record<string, unknown>) => {
const val = (d.count as number) || 0;
const pct = (val / maxVal) * 100;
const label = d.label || d.date || d.key || "";
return `
<div style="display:flex;align-items:center;gap:8px;margin:2px 0;">
<div style="width:80px;font-size:11px;color:#888;text-align:right;">${escapeHtml(String(label))}</div>
<div style="flex:1;background:#111;border-radius:4px;height:20px;">
<div style="width:${pct}%;background:linear-gradient(90deg,#4ecdc4,#44a8a0);height:100%;border-radius:4px;min-width:2px;"></div>
</div>
<div style="width:40px;font-size:11px;color:#aaa;">${val}</div>
</div>`;
})
.join("");
bookingChartHtml = `
<div style="margin-top:20px;">
<h3 style="color:#ccc;margin:0 0 12px;">📊 Booking Trend</h3>
${bars}
</div>`;
}
// Metric data summary
let metricHtml = "";
if (metricData && typeof metricData === "object") {
metricHtml = `
<div style="margin-top:20px;">
<h3 style="color:#ccc;margin:0 0 8px;">📈 Response Metric Data</h3>
<pre style="background:#111;padding:12px;border-radius:8px;font-size:11px;color:#aaa;overflow-x:auto;max-height:200px;">${escapeHtml(JSON.stringify(metricData, null, 2).slice(0, 2000))}</pre>
</div>`;
}
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 16px;color:#fff;">📊 Analytics Dashboard</h2>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
${cardHtml}
</div>
${bookingChartHtml}
${metricHtml}
</div>`;
}
export async function handler(
client: CloseBotClient,
name: string,
args: Record<string, unknown>
): Promise<ToolResult> {
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<AgencyDashboardSummaryResponse>("/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);
}
}

135
src/apps/bot-dashboard.ts Normal file
View File

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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 `
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:8px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;font-size:15px;color:#e0e0e0;">${fav} ${escapeHtml(bot.name || "Unnamed")} ${locked}</span>
<span style="font-size:12px;color:#888;">${escapeHtml(category)}</span>
</div>
<div style="display:flex;gap:12px;font-size:13px;color:#aaa;">
<span>${published} v${escapeHtml(versionLabel)}</span>
<span>📡 ${sourceCount} source${sourceCount !== 1 ? "s" : ""}</span>
</div>
<div style="font-size:12px;color:#666;">Modified: ${escapeHtml(modified)}</div>
<div style="font-size:11px;color:#555;font-family:monospace;">ID: ${escapeHtml(bot.id || "")}</div>
${bot.followUpActive ? '<div style="font-size:11px;color:#4ecdc4;">↻ Follow-ups active</div>' : ""}
${bot.tools && bot.tools.length > 0 ? `<div style="font-size:11px;color:#8888ff;">🔧 ${bot.tools.length} tool(s)</div>` : ""}
</div>`;
}).join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 16px;color:#fff;">🤖 Bot Dashboard <span style="font-size:14px;color:#888;">(${bots.length} bots)</span></h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;">
${botCards}
</div>
</div>`;
}
function renderBotDetail(bot: BotDto): string {
const versions = (bot.versions || [])
.map(
(v) =>
`<tr><td style="padding:4px 12px;">${escapeHtml(v.version || "")}</td><td>${v.published ? "✅ Published" : "📝 Draft"}</td><td>${escapeHtml(v.name || "—")}</td><td>${v.modifiedAt ? new Date(v.modifiedAt).toLocaleString() : "—"}</td></tr>`
)
.join("");
const sources = (bot.sources || [])
.map(
(s) =>
`<tr><td style="padding:4px 12px;">${escapeHtml(s.name || "Unnamed")}</td><td>${escapeHtml(s.category || "—")}</td><td>${s.enabled ? "✅" : "❌"}</td><td style="font-family:monospace;font-size:11px;">${escapeHtml(s.id || "")}</td></tr>`
)
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 4px;color:#fff;">🤖 ${escapeHtml(bot.name || "Unnamed")}</h2>
<div style="font-size:12px;color:#666;margin-bottom:16px;font-family:monospace;">ID: ${escapeHtml(bot.id || "")}</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;">
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
<div style="font-size:24px;">${bot.sources?.length || 0}</div>
<div style="font-size:12px;color:#888;">Sources</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
<div style="font-size:24px;">${bot.versions?.length || 0}</div>
<div style="font-size:12px;color:#888;">Versions</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
<div style="font-size:24px;">${bot.locked ? "🔒" : "🔓"}</div>
<div style="font-size:12px;color:#888;">${bot.locked ? "Locked" : "Unlocked"}</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
<div style="font-size:24px;">${bot.followUpActive ? "✅" : "❌"}</div>
<div style="font-size:12px;color:#888;">Follow-ups</div>
</div>
</div>
<h3 style="color:#ccc;margin:16px 0 8px;">📋 Versions</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<tr style="background:#1a1a2e;"><th style="text-align:left;padding:8px 12px;">Version</th><th>Status</th><th>Name</th><th>Modified</th></tr>
${versions || '<tr><td colspan="4" style="padding:8px;color:#666;">No versions</td></tr>'}
</table>
<h3 style="color:#ccc;margin:16px 0 8px;">📡 Sources</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<tr style="background:#1a1a2e;"><th style="text-align:left;padding:8px 12px;">Name</th><th>Category</th><th>Enabled</th><th>ID</th></tr>
${sources || '<tr><td colspan="4" style="padding:8px;color:#666;">No sources attached</td></tr>'}
</table>
</div>`;
}
export async function handler(
client: CloseBotClient,
name: string,
args: Record<string, unknown>
): Promise<ToolResult> {
try {
if (args.botId) {
const bot = await client.get<BotDto>(`/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<BotDto[]>("/bot");
const html = renderBotGrid(bots);
return {
content: [{ type: "text", text: `Dashboard showing ${bots.length} bots` }],
structuredContent: { type: "html", html },
};
} catch (error) {
return err(error);
}
}

184
src/apps/lead-manager.ts Normal file
View File

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function renderLeadTable(data: LeadDtoPaginated): string {
const leads = data.results || [];
const rows = leads
.map((lead) => {
const lastMsg = lead.lastMessage
? escapeHtml(lead.lastMessage.slice(0, 50))
: '<span style="color:#555;">—</span>';
const direction =
lead.lastMessageDirection === "outbound"
? '<span style="color:#4ecdc4;">→ out</span>'
: lead.lastMessageDirection === "inbound"
? '<span style="color:#ff9f43;">← in</span>'
: '<span style="color:#555;">—</span>';
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
? `<div style="font-size:10px;color:#ff6b6b;margin-top:2px;">⚠️ ${escapeHtml(lead.mostRecentFailureReason.slice(0, 40))}</div>`
: "";
const tags =
lead.tags && lead.tags.length > 0
? lead.tags
.slice(0, 3)
.map((t) => `<span style="background:#2a1a3e;padding:1px 6px;border-radius:4px;font-size:10px;">${escapeHtml(t)}</span>`)
.join(" ")
: "";
return `
<tr style="border-bottom:1px solid #222;">
<td style="padding:10px 8px;">
<div style="font-weight:600;font-size:13px;">${escapeHtml(lead.name || "Unknown")}</div>
<div style="font-size:10px;color:#555;font-family:monospace;">${escapeHtml(lead.id || "")}</div>
${tags ? `<div style="margin-top:4px;">${tags}</div>` : ""}
</td>
<td style="padding:10px 8px;font-size:12px;color:#888;">${escapeHtml(source)}</td>
<td style="padding:10px 8px;font-size:12px;">
${direction}
<div style="font-size:11px;color:#aaa;margin-top:2px;">${lastMsg}</div>
${failReason}
</td>
<td style="padding:10px 8px;font-size:11px;color:#666;">${escapeHtml(time)}</td>
<td style="padding:10px 8px;text-align:center;font-size:12px;">${fieldCount}</td>
<td style="padding:10px 8px;text-align:center;font-size:12px;">${instanceCount}</td>
</tr>`;
})
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 4px;color:#fff;">👥 Lead Manager</h2>
<div style="font-size:12px;color:#666;margin-bottom:16px;">
${data.total ?? leads.length} total leads · Page ${(data.page ?? 0) + 1} · ${data.pageSize ?? 20} per page
</div>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#1a1a2e;">
<th style="text-align:left;padding:10px 8px;font-size:12px;color:#888;">Lead</th>
<th style="text-align:left;padding:10px 8px;font-size:12px;color:#888;">Source</th>
<th style="text-align:left;padding:10px 8px;font-size:12px;color:#888;">Last Message</th>
<th style="text-align:left;padding:10px 8px;font-size:12px;color:#888;">Time</th>
<th style="text-align:center;padding:10px 8px;font-size:12px;color:#888;">Fields</th>
<th style="text-align:center;padding:10px 8px;font-size:12px;color:#888;">Bots</th>
</tr>
</thead>
<tbody>
${rows || '<tr><td colspan="6" style="padding:40px;text-align:center;color:#666;">No leads found</td></tr>'}
</tbody>
</table>
</div>
</div>`;
}
function renderLeadDetail(lead: LeadDto): string {
const fields = (lead.fields || [])
.map(
(f) =>
`<tr><td style="padding:4px 8px;color:#888;font-size:12px;">${escapeHtml(f.field || "")}</td><td style="padding:4px 8px;font-size:12px;">${escapeHtml(f.value || "—")}</td></tr>`
)
.join("");
const instances = (lead.instances || [])
.map(
(i) =>
`<tr><td style="padding:4px 8px;font-size:12px;font-family:monospace;">${escapeHtml(i.botId || "")}</td><td style="padding:4px 8px;font-size:12px;">v${escapeHtml(i.botVersion || "?")}</td><td style="padding:4px 8px;font-size:12px;">${i.followUpTime ? new Date(i.followUpTime).toLocaleString() : "—"}</td></tr>`
)
.join("");
const tags = (lead.tags || [])
.map((t) => `<span style="background:#2a1a3e;padding:2px 8px;border-radius:6px;font-size:11px;margin:2px;">${escapeHtml(t)}</span>`)
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 4px;color:#fff;">👤 ${escapeHtml(lead.name || "Unknown Lead")}</h2>
<div style="font-size:12px;color:#555;font-family:monospace;margin-bottom:16px;">${escapeHtml(lead.id || "")}</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px;">
<div style="background:#1a1a2e;padding:12px;border-radius:8px;">
<div style="font-size:11px;color:#888;">Source</div>
<div style="font-size:14px;margin-top:4px;">${escapeHtml(lead.source?.name || "—")}</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;">
<div style="font-size:11px;color:#888;">Last Message</div>
<div style="font-size:12px;margin-top:4px;color:#aaa;">${escapeHtml((lead.lastMessage || "—").slice(0, 80))}</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;">
<div style="font-size:11px;color:#888;">Contact ID</div>
<div style="font-size:12px;margin-top:4px;font-family:monospace;">${escapeHtml(lead.contactId || "—")}</div>
</div>
</div>
${tags ? `<div style="margin-bottom:16px;">${tags}</div>` : ""}
${lead.mostRecentFailureReason ? `<div style="background:#2a1a1a;border:1px solid #5a2a2a;border-radius:8px;padding:12px;margin-bottom:16px;font-size:12px;color:#ff6b6b;">⚠️ ${escapeHtml(lead.mostRecentFailureReason)}</div>` : ""}
<h3 style="color:#ccc;margin:16px 0 8px;">📋 Fields (${lead.fields?.length || 0})</h3>
<table style="width:100%;border-collapse:collapse;">
${fields || '<tr><td style="padding:8px;color:#666;">No fields</td></tr>'}
</table>
<h3 style="color:#ccc;margin:16px 0 8px;">🤖 Bot Instances (${lead.instances?.length || 0})</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="background:#1a1a2e;"><th style="text-align:left;padding:6px 8px;font-size:11px;">Bot ID</th><th>Version</th><th>Follow-up</th></tr>
${instances || '<tr><td colspan="3" style="padding:8px;color:#666;">No instances</td></tr>'}
</table>
</div>`;
}
export async function handler(
client: CloseBotClient,
name: string,
args: Record<string, unknown>
): Promise<ToolResult> {
try {
if (args.leadId) {
const lead = await client.get<LeadDto>(`/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<LeadDtoPaginated>("/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);
}
}

157
src/apps/leaderboard.ts Normal file
View File

@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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
? ' <span style="background:#4299e1;color:#fff;padding:1px 6px;border-radius:8px;font-size:11px">YOU</span>'
: "";
return `<tr${isYou}><td style="text-align:center;font-size:18px">${medal}</td><td>${name}${youBadge}</td><td style="text-align:right;font-variant-numeric:tabular-nums">${value.toLocaleString()}</td></tr>`;
})
.join("\n");
return `<div style="font-family:system-ui,-apple-system,sans-serif;background:#1a202c;color:#e2e8f0;padding:20px;border-radius:12px;max-width:600px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<div>
<h2 style="margin:0;font-size:20px;color:#fff">${scopeLabel} Leaderboard</h2>
<span style="color:#a0aec0;font-size:13px">${metricLabel} · ${dateRange}</span>
</div>
<div style="display:flex;gap:6px">
<span style="background:${metric === "responses" ? "#4299e1" : "#2d3748"};color:#fff;padding:4px 10px;border-radius:6px;font-size:12px;cursor:pointer">Responses</span>
<span style="background:${metric === "bookings" ? "#48bb78" : "#2d3748"};color:#fff;padding:4px 10px;border-radius:6px;font-size:12px;cursor:pointer">Bookings</span>
<span style="background:${metric === "contacts" ? "#ed8936" : "#2d3748"};color:#fff;padding:4px 10px;border-radius:6px;font-size:12px;cursor:pointer">Contacts</span>
</div>
</div>
<table style="width:100%;border-collapse:collapse">
<thead>
<tr style="border-bottom:1px solid #4a5568">
<th style="text-align:center;padding:8px 4px;color:#a0aec0;font-size:12px;width:50px">Rank</th>
<th style="text-align:left;padding:8px;color:#a0aec0;font-size:12px">Agency</th>
<th style="text-align:right;padding:8px;color:#a0aec0;font-size:12px">${metricLabel}</th>
</tr>
</thead>
<tbody>
${rows || '<tr><td colspan="3" style="text-align:center;padding:20px;color:#718096">No leaderboard data available</td></tr>'}
</tbody>
</table>
<div style="margin-top:12px;text-align:center;color:#718096;font-size:11px">${entries.length} agencies shown</div>
</div>`;
}
export async function handle(
client: CloseBotClient,
name: string,
args: Record<string, unknown>
): Promise<ToolResult> {
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<LeaderboardResponse[]>(
"/botMetric/localleaderboard",
{
metric,
start,
end,
numSurroundingAgencies: args.numSurrounding ?? 5,
}
);
} else {
entries = await client.get<LeaderboardResponse[]>(
"/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);
}
}

192
src/apps/library-manager.ts Normal file
View File

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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 '<span style="background:#1a3a2a;color:#4ecdc4;padding:2px 8px;border-radius:4px;font-size:10px;">✅ Ready</span>';
if (s === "processing" || s === "pending")
return '<span style="background:#3a3a1a;color:#ffd93d;padding:2px 8px;border-radius:4px;font-size:10px;">⏳ Processing</span>';
if (s === "error" || s === "failed")
return '<span style="background:#3a1a1a;color:#ff6b6b;padding:2px 8px;border-radius:4px;font-size:10px;">❌ Error</span>';
return `<span style="background:#1a1a2e;color:#888;padding:2px 8px;border-radius:4px;font-size:10px;">${escapeHtml(status || "Unknown")}</span>`;
}
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) =>
`<span style="background:#1a2a3e;padding:1px 6px;border-radius:4px;font-size:10px;color:#7ec8e3;">${escapeHtml(s.name || s.id || "")}</span>`
)
.join(" ");
const modified = f.lastModified
? new Date(f.lastModified).toLocaleDateString()
: "—";
return `
<div style="background:#1a1a2e;border-radius:10px;padding:14px;display:flex;align-items:center;gap:14px;">
<div style="font-size:28px;width:36px;text-align:center;">${icon}</div>
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:13px;color:#e0e0e0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
${escapeHtml(f.fileName || "Unnamed")}
</div>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px;">
${statusBadge(f.fileStatus)}
<span style="font-size:11px;color:#666;">${escapeHtml(f.fileType || "")}</span>
<span style="font-size:11px;color:#666;">${formatSize(f.fileSize || 0)}</span>
<span style="font-size:11px;color:#666;">${escapeHtml(modified)}</span>
</div>
${sources ? `<div style="margin-top:6px;display:flex;gap:4px;flex-wrap:wrap;">${sources}</div>` : ""}
</div>
<div style="font-size:10px;color:#444;font-family:monospace;white-space:nowrap;">${escapeHtml(f.fileId || "")}</div>
</div>`;
})
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 4px;color:#fff;">📚 Library Manager</h2>
<div style="font-size:12px;color:#666;margin-bottom:16px;">
${files.length} files · ${formatSize(totalSize)} total
</div>
<div style="display:flex;flex-direction:column;gap:8px;">
${rows || '<div style="padding:40px;text-align:center;color:#666;">No files. Use upload_file or create_web_scrape to add content.</div>'}
</div>
<div style="margin-top:12px;font-size:11px;color:#555;">
💡 Use <code>upload_file</code> to upload, <code>create_web_scrape</code> to scrape websites, <code>attach_file_to_source</code> to connect to sources
</div>
</div>`;
}
function renderFileDetail(file: FileDto, scrapePages: Array<{ url?: string; enabled?: boolean }>): string {
const sources = (file.sources || [])
.map(
(s) =>
`<div style="background:#1a2a3e;padding:8px 12px;border-radius:6px;display:flex;justify-content:space-between;">
<span style="font-size:12px;color:#7ec8e3;">${escapeHtml(s.name || "Unnamed")}</span>
<span style="font-size:10px;color:#666;">${escapeHtml(s.category || "")} · ${escapeHtml(s.id || "")}</span>
</div>`
)
.join("");
const pages = scrapePages
.map(
(p) =>
`<div style="display:flex;align-items:center;gap:8px;padding:4px 0;">
<span style="font-size:14px;">${p.enabled ? "✅" : "❌"}</span>
<span style="font-size:12px;color:#aaa;word-break:break-all;">${escapeHtml(p.url || "")}</span>
</div>`
)
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<span style="font-size:36px;">${fileIcon(file.fileType)}</span>
<div>
<h2 style="margin:0;color:#fff;">${escapeHtml(file.fileName || "Unnamed")}</h2>
<div style="font-size:11px;color:#555;font-family:monospace;">${escapeHtml(file.fileId || "")}</div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;">
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
<div style="font-size:11px;color:#888;">Status</div>
<div style="margin-top:4px;">${statusBadge(file.fileStatus)}</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
<div style="font-size:11px;color:#888;">Type</div>
<div style="font-size:13px;margin-top:4px;">${escapeHtml(file.fileType || "—")}</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
<div style="font-size:11px;color:#888;">Size</div>
<div style="font-size:13px;margin-top:4px;">${formatSize(file.fileSize || 0)}</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
<div style="font-size:11px;color:#888;">Modified</div>
<div style="font-size:13px;margin-top:4px;">${file.lastModified ? new Date(file.lastModified).toLocaleDateString() : "—"}</div>
</div>
</div>
<h3 style="color:#ccc;margin:16px 0 8px;">📡 Attached Sources (${file.sources?.length || 0})</h3>
<div style="display:flex;flex-direction:column;gap:4px;">
${sources || '<div style="color:#666;font-size:12px;">Not attached to any sources</div>'}
</div>
${scrapePages.length > 0 ? `
<h3 style="color:#ccc;margin:16px 0 8px;">🌐 Scrape Pages (${scrapePages.length})</h3>
<div style="background:#111;border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;">
${pages}
</div>
` : ""}
</div>`;
}
export async function handler(
client: CloseBotClient,
name: string,
args: Record<string, unknown>
): Promise<ToolResult> {
try {
if (args.fileId) {
const [file, scrapePages] = await Promise.all([
client.get<FileDto>(`/library/files/${args.fileId}`),
client.get<Array<{ url?: string; enabled?: boolean }>>(`/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<FileDto[]>("/library/files");
const html = renderFileList(files);
return {
content: [{ type: "text", text: `Library: ${files.length} files` }],
structuredContent: { type: "html", html },
};
} catch (error) {
return err(error);
}
}

145
src/apps/test-console.ts Normal file
View File

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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 `
<div style="background:#1a1a2e;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:4px;">
<div style="display:flex;justify-content:space-between;">
<span style="font-weight:600;color:#e0e0e0;font-size:13px;">${escapeHtml(lead.name || "Test Lead")}</span>
<span style="font-size:11px;color:#666;">${time}</span>
</div>
<div style="font-size:12px;color:#888;">${direction} ${lastMsg}${(lead.lastMessage?.length ?? 0) > 60 ? "…" : ""}</div>
<div style="font-size:10px;color:#555;font-family:monospace;">Lead: ${escapeHtml(lead.id || "")}</div>
</div>`;
})
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 4px;color:#fff;">🧪 Test Console</h2>
<div style="font-size:12px;color:#666;margin-bottom:16px;">Bot: <code>${escapeHtml(botId)}</code> · ${sessions.total ?? leads.length} session(s)</div>
<div style="display:flex;flex-direction:column;gap:8px;">
${rows || '<div style="color:#666;padding:20px;text-align:center;">No test sessions found. Create one with create_test_session.</div>'}
</div>
</div>`;
}
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
? `<div style="font-size:10px;color:#666;margin-top:4px;">Activities: ${msg.activities.map((a) => escapeHtml(a.activity || "")).join(", ")}</div>`
: "";
const attachments =
msg.attachments && msg.attachments.length > 0
? `<div style="font-size:10px;color:#4ecdc4;margin-top:4px;">📎 ${msg.attachments.length} attachment(s)</div>`
: "";
return `
<div style="display:flex;justify-content:${align};margin:4px 0;">
<div style="max-width:75%;background:${bg};border:1px solid ${border};border-radius:12px;padding:10px 14px;">
<div style="display:flex;justify-content:space-between;gap:16px;margin-bottom:4px;">
<span style="font-size:11px;font-weight:600;color:#aaa;">${label}</span>
<span style="font-size:10px;color:#555;">${escapeHtml(time)}</span>
</div>
<div style="font-size:13px;color:#e0e0e0;white-space:pre-wrap;">${escapeHtml(content)}</div>
${activities}${attachments}
<div style="font-size:9px;color:#444;margin-top:4px;font-family:monospace;">msg: ${escapeHtml(msg.messageId || "")}</div>
</div>
</div>`;
})
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 4px;color:#fff;">🧪 Test Conversation</h2>
<div style="font-size:12px;color:#666;margin-bottom:16px;">
Bot: <code>${escapeHtml(botId)}</code> · Lead: <code>${escapeHtml(leadId)}</code> · ${sorted.length} messages
</div>
<div style="background:#111;border-radius:12px;padding:16px;max-height:600px;overflow-y:auto;">
${msgHtml || '<div style="color:#666;text-align:center;padding:40px;">No messages yet. Send one with send_test_message.</div>'}
</div>
<div style="margin-top:12px;display:flex;gap:8px;font-size:11px;color:#888;">
<span>💡 Use <code>send_test_message</code> to send messages</span>
<span>· <code>force_test_step</code> to advance</span>
<span>· <code>rollback_test_session</code> to undo</span>
</div>
</div>`;
}
export async function handler(
client: CloseBotClient,
name: string,
args: Record<string, unknown>
): Promise<ToolResult> {
try {
const botId = args.botId as string;
const leadId = args.leadId as string | undefined;
if (leadId) {
const messages = await client.get<BotMetricMessage[]>("/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<ListLeadDto>(`/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);
}
}

210
src/client.ts Normal file
View File

@ -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, unknown>): 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<string, string> {
return {
"X-CB-KEY": this.apiKey,
"Content-Type": "application/json",
Accept: "application/json",
};
}
async get<T = unknown>(
path: string,
query?: Record<string, unknown>
): Promise<T> {
const url = this.buildUrl(path, query);
const response = await fetch(url, {
method: "GET",
headers: this.headers,
});
return this.handleResponse<T>(response);
}
async post<T = unknown>(
path: string,
body?: unknown,
query?: Record<string, unknown>
): Promise<T> {
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<T>(response);
}
async put<T = unknown>(
path: string,
body?: unknown,
query?: Record<string, unknown>
): Promise<T> {
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<T>(response);
}
async delete<T = unknown>(
path: string,
query?: Record<string, unknown>
): Promise<T> {
const url = this.buildUrl(path, query);
const response = await fetch(url, {
method: "DELETE",
headers: this.headers,
});
return this.handleResponse<T>(response);
}
async postFormData<T = unknown>(
path: string,
formData: FormData,
query?: Record<string, unknown>
): Promise<T> {
const url = this.buildUrl(path, query);
const headers: Record<string, string> = {
"X-CB-KEY": this.apiKey,
Accept: "application/json",
};
const response = await fetch(url, {
method: "POST",
headers,
body: formData,
});
return this.handleResponse<T>(response);
}
async putFormData<T = unknown>(
path: string,
formData: FormData,
query?: Record<string, unknown>
): Promise<T> {
const url = this.buildUrl(path, query);
const headers: Record<string, string> = {
"X-CB-KEY": this.apiKey,
Accept: "application/json",
};
const response = await fetch(url, {
method: "PUT",
headers,
body: formData,
});
return this.handleResponse<T>(response);
}
private async handleResponse<T>(response: Response): Promise<T> {
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,
};
}

156
src/index.ts Normal file
View File

@ -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<string, unknown>
) => Promise<ToolResult>;
}
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<string, number>();
// All tool definitions (populated on init)
let allTools: ToolDefinition[] = [];
async function loadGroupMetadata(): Promise<void> {
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<ToolModule["handle"] | null> {
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<void> {
// 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<string, unknown>;
}
const result = await handler(client, name, (args as Record<string, unknown>) || {});
return result as unknown as Record<string, unknown>;
});
// --- 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);
});

353
src/tools/agency-billing.ts Normal file
View File

@ -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<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
case "get_agency":
return ok(await client.get<AgencyDto>("/agency/current"));
case "list_agencies":
return ok(await client.get<AgencyDto[]>("/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<BalanceDto>("/agency/billing/balance"));
case "get_source_balance":
return ok(
await client.get<BalanceDto>(
`/agency/billing/balance/source/${args.sourceId}`
)
);
case "get_billing_options":
return ok(
await client.get<BillingOptionsDto>("/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<BillingOptionsDto>("/agency/billing/options", body)
);
}
case "list_transactions":
return ok(
await client.get<TransactionDto[]>("/agency/billing/transactions", {
status: args.status,
page: args.page,
pageSize: args.pageSize,
})
);
case "list_source_transactions":
return ok(
await client.get<TransactionDto[]>(
`/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<TransactionDto>(
`/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<BilledUsageDto[]>("/agency/billing/usages", {
startTime: args.startTime,
endTime: args.endTime,
})
);
case "get_rebilling":
return ok(
await client.get<ReBillingDto>("/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);
}
}

344
src/tools/analytics.ts Normal file
View File

@ -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<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
case "get_agency_summary":
return ok(
await client.get<AgencyDashboardSummaryResponse>("/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<number>("/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<BotMetricAction[]>("/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<number>("/botMetric/messageCount", {
sourceId: args.sourceId,
leadId: args.leadId,
start: args.start,
end: args.end,
})
);
case "get_messages":
return ok(
await client.get<BotMetricMessage[]>("/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<MessageFeedbackResponse>("/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<MessageLikesResponse>("/botMetric/messageLikes", {
leadId: args.leadId,
})
);
case "get_message_reason":
return ok(
await client.get<MessageReason>("/botMetric/messageReason", {
messageId: args.messageId,
})
);
case "get_leaderboard":
return ok(
await client.get<LeaderboardResponse[]>("/botMetric/leaderboard", {
metric: args.metric,
start: args.start,
end: args.end,
numTopLeaders: args.numTopLeaders,
})
);
case "get_local_leaderboard":
return ok(
await client.get<LeaderboardResponse[]>("/botMetric/localleaderboard", {
metric: args.metric,
start: args.start,
end: args.end,
numSurroundingAgencies: args.numSurroundingAgencies,
})
);
case "get_logs":
return ok(
await client.get<BotMetricLog[]>("/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);
}
}

353
src/tools/bot-management.ts Normal file
View File

@ -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<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
case "list_bots":
return ok(await client.get<BotDto[]>("/bot"));
case "get_bot":
return ok(await client.get<BotDto>(`/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<BotDto>("/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<BotDto>("/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<BotDto>(`/bot/${id}`, body));
}
case "delete_bot":
return ok(await client.delete(`/bot/${args.id}`));
case "duplicate_bot":
return ok(await client.post<BotDto>(`/bot/${args.id}/duplicate`));
case "export_bot":
return ok(
await client.get<ExportBotResponse>(`/bot/${args.id}/export`, {
botVersion: args.botVersion,
})
);
case "publish_bot":
return ok(await client.post<PublishBotResponse>(`/bot/${args.id}/publish`));
case "save_bot": {
const body: SaveBotInput = { botSteps: args.botSteps };
return ok(await client.post<SaveBotResponse>(`/bot/${args.id}/save`, body));
}
case "save_bot_tools":
return ok(
await client.post<BotToolDto[]>(
`/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<BotTemplateDto[]>("/botTemplates"));
case "attach_source_to_bot": {
const body: Record<string, unknown> = {};
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<string[]>("/bot/bbb/templates"));
default:
return err(`Unknown tool: ${name}`);
}
} catch (error) {
return err(error);
}
}

166
src/tools/bot-testing.ts Normal file
View File

@ -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<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
case "create_test_session":
return ok(
await client.post<TestSession>(`/bot/${args.botId}/testSession`)
);
case "list_test_sessions":
return ok(
await client.get<ListLeadDto>(`/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<UpdateSessionDto>(
`/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<BotTestingRollbackOutput>(
`/bot/${args.botId}/testSession/${args.leadId}/rollback`,
body
)
);
}
default:
return err(`Unknown tool: ${name}`);
}
} catch (error) {
return err(error);
}
}

539
src/tools/configuration.ts Normal file
View File

@ -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<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
// Personas
case "list_personas":
return ok(await client.get<PersonaDto[]>("/persona"));
case "get_persona":
return ok(await client.get<PersonaDto>(`/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<string, unknown>)[k] = v;
}
return ok(await client.put<PersonaDto>(`/persona/${id}`, body));
}
case "delete_persona":
return ok(await client.delete(`/persona/${args.id}`));
// FAQs
case "list_faqs":
return ok(
await client.get<SmartFAQDto[]>("/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<SmartFAQDto[]>("/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<ListHierarchyResult[]>("/hierarchy"));
case "get_folder":
return ok(await client.get<ListHierarchyResult>(`/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<NotificationDto[]>("/notifications"));
case "update_notification": {
const body: NotificationUpdateDto = { viewed: args.viewed as boolean };
return ok(await client.put<NotificationDto>(`/notifications/${args.id}`, body));
}
case "delete_notification":
return ok(await client.delete<NotificationDto>(`/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<ApiKeyDTO[]>("/account/apiKey"));
case "create_api_key": {
const body: CreateApiKeyInput = { name: args.name as string };
return ok(await client.post<CreateApiKeyOutput>("/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<LiveDemoDto[]>(`/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<LiveDemoDto>(`/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<LiveDemoDto>(`/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<BotVariableDto[]>(
`/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);
}
}

View File

@ -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<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
case "list_leads":
return ok(
await client.get<LeadDtoPaginated>("/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<LeadDto[]>("/lead/search", body));
}
case "get_lead":
return ok(await client.get<LeadDto>(`/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);
}
}

232
src/tools/library.ts Normal file
View File

@ -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<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
case "list_files":
return ok(await client.get<FileDto[]>("/library/files"));
case "get_file":
return ok(await client.get<FileDto>(`/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<WebscrapePageDto[]>(
`/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);
}
}

View File

@ -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<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
case "list_sources":
return ok(
await client.get<SourceDtoPaginated>("/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<SourceDto>(`/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<SourceDto>("/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<SourceDto>(`/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<SourceCalendarDto[]>(`/agency/source/${args.id}/calendars`)
);
case "list_source_channels":
return ok(
await client.get<SourceChannelDto[]>(`/agency/source/${args.id}/channels`)
);
case "list_source_fields":
return ok(
await client.get<SourceFieldCollectionDto>(`/agency/source/${args.id}/fields`)
);
case "list_source_tags":
return ok(
await client.get<SourceTagDto[]>(`/agency/source/${args.id}/tags`)
);
default:
return err(`Unknown tool: ${name}`);
}
} catch (error) {
return err(error);
}
}

940
src/types.ts Normal file
View File

@ -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<string, unknown>;
}
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<string, unknown>;
required?: string[];
};
}
export interface ToolGroup {
tools: ToolDefinition[];
handler: (name: string, args: Record<string, unknown>) => Promise<ToolResult>;
}
export interface ToolResult {
content: Array<{ type: "text"; text: string } | { type: "text"; text: string; [key: string]: unknown }>;
isError?: boolean;
structuredContent?: unknown;
}

20
tsconfig.json Normal file
View File

@ -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"]
}