Initial commit: Leexi MCP server (read-only, 6 tools)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export const API_BASE_URL = "https://public-api.leexi.ai/v1";
|
||||
export const CHARACTER_LIMIT = 25_000;
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const MAX_PAGE_SIZE = 100;
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Leexi MCP Server
|
||||
*
|
||||
* Read-only MCP server for the Leexi public API.
|
||||
* Provides access to meeting notes, transcripts, action items, and call metadata.
|
||||
*
|
||||
* Environment variables:
|
||||
* LEEXI_KEY_ID — API key ID (from https://app.leexi.ai/settings/api_keys)
|
||||
* LEEXI_KEY_SECRET — API key secret
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { registerUserTools } from "./tools/users.js";
|
||||
import { registerCallTools } from "./tools/calls.js";
|
||||
import { registerMeetingTools } from "./tools/meetings.js";
|
||||
|
||||
const server = new McpServer({
|
||||
name: "leexi-mcp-server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// Register all tools
|
||||
registerUserTools(server);
|
||||
registerCallTools(server);
|
||||
registerMeetingTools(server);
|
||||
|
||||
// Run with stdio transport
|
||||
async function main(): Promise<void> {
|
||||
if (!process.env.LEEXI_KEY_ID || !process.env.LEEXI_KEY_SECRET) {
|
||||
console.error(
|
||||
"ERROR: LEEXI_KEY_ID and LEEXI_KEY_SECRET environment variables are required.\n" +
|
||||
"Generate API keys at https://app.leexi.ai/settings/api_keys"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("leexi-mcp-server running via stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import { z } from "zod";
|
||||
import { ResponseFormat } from "../types.js";
|
||||
|
||||
// ─── Shared ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const responseFormatField = z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for structured data");
|
||||
|
||||
const pageField = z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.default(1)
|
||||
.describe("Page number (1-based)");
|
||||
|
||||
const itemsField = z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.default(20)
|
||||
.describe("Results per page (1–100)");
|
||||
|
||||
// ─── Users ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const ListUsersInputSchema = z
|
||||
.object({
|
||||
page: pageField,
|
||||
items: itemsField,
|
||||
response_format: responseFormatField,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ListUsersInput = z.infer<typeof ListUsersInputSchema>;
|
||||
|
||||
// ─── Teams ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const ListTeamsInputSchema = z
|
||||
.object({
|
||||
page: pageField,
|
||||
items: itemsField,
|
||||
response_format: responseFormatField,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ListTeamsInput = z.infer<typeof ListTeamsInputSchema>;
|
||||
|
||||
// ─── Calls (list) ────────────────────────────────────────────────────────────
|
||||
|
||||
export const ListCallsInputSchema = z
|
||||
.object({
|
||||
page: pageField,
|
||||
items: itemsField,
|
||||
order: z
|
||||
.enum([
|
||||
"created_at desc",
|
||||
"created_at asc",
|
||||
"performed_at desc",
|
||||
"performed_at asc",
|
||||
"updated_at desc",
|
||||
"updated_at asc",
|
||||
])
|
||||
.default("performed_at desc")
|
||||
.describe("Sort order"),
|
||||
date_filter: z
|
||||
.enum(["created_at", "performed_at", "updated_at"])
|
||||
.default("performed_at")
|
||||
.describe("Which date field the from/to filters apply to"),
|
||||
from: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Start date filter (ISO 8601, e.g. 2024-06-01T00:00:00.000Z)"),
|
||||
to: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("End date filter (ISO 8601)"),
|
||||
source: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Integration slug filter (e.g. 'aircall', 'gmeet_bot')"),
|
||||
owner_uuid: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Filter by owner user UUID(s)"),
|
||||
participating_user_uuid: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Filter by participating user UUID(s)"),
|
||||
customer_email_address: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Filter by customer email address(es)"),
|
||||
customer_phone_number: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Filter by customer phone number(s)"),
|
||||
with_simple_transcript: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe("Include paragraph-level transcript in list results"),
|
||||
response_format: responseFormatField,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ListCallsInput = z.infer<typeof ListCallsInputSchema>;
|
||||
|
||||
// ─── Call (detail) ───────────────────────────────────────────────────────────
|
||||
|
||||
export const GetCallInputSchema = z
|
||||
.object({
|
||||
uuid: z.string().describe("Call UUID"),
|
||||
response_format: responseFormatField,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type GetCallInput = z.infer<typeof GetCallInputSchema>;
|
||||
|
||||
// ─── Meeting Events (list) ───────────────────────────────────────────────────
|
||||
|
||||
export const ListMeetingEventsInputSchema = z
|
||||
.object({
|
||||
page: pageField,
|
||||
items: itemsField,
|
||||
order: z
|
||||
.enum([
|
||||
"created_at desc",
|
||||
"created_at asc",
|
||||
"start_time desc",
|
||||
"start_time asc",
|
||||
"end_time desc",
|
||||
"end_time asc",
|
||||
])
|
||||
.default("start_time desc")
|
||||
.describe("Sort order"),
|
||||
created_by: z
|
||||
.enum(["calendar", "manual", "api"])
|
||||
.optional()
|
||||
.describe("Filter by origin type"),
|
||||
response_format: responseFormatField,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ListMeetingEventsInput = z.infer<typeof ListMeetingEventsInputSchema>;
|
||||
|
||||
// ─── Meeting Event (detail) ──────────────────────────────────────────────────
|
||||
|
||||
export const GetMeetingEventInputSchema = z
|
||||
.object({
|
||||
uuid: z.string().describe("Meeting event UUID"),
|
||||
response_format: responseFormatField,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type GetMeetingEventInput = z.infer<typeof GetMeetingEventInputSchema>;
|
||||
@@ -0,0 +1,74 @@
|
||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { API_BASE_URL } from "../constants.js";
|
||||
|
||||
let client: AxiosInstance | null = null;
|
||||
|
||||
export function getClient(): AxiosInstance {
|
||||
if (client) return client;
|
||||
|
||||
const keyId = process.env.LEEXI_KEY_ID;
|
||||
const keySecret = process.env.LEEXI_KEY_SECRET;
|
||||
|
||||
if (!keyId || !keySecret) {
|
||||
throw new Error(
|
||||
"Missing LEEXI_KEY_ID or LEEXI_KEY_SECRET environment variables. " +
|
||||
"Generate API keys at https://app.leexi.ai/settings/api_keys"
|
||||
);
|
||||
}
|
||||
|
||||
const encoded = Buffer.from(`${keyId}:${keySecret}`).toString("base64");
|
||||
|
||||
client = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 30_000,
|
||||
headers: {
|
||||
Authorization: `Basic ${encoded}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function apiGet<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, unknown>
|
||||
): Promise<T> {
|
||||
const http = getClient();
|
||||
const response = await http.get<T>(endpoint, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export function handleApiError(error: unknown): string {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const body =
|
||||
typeof error.response.data === "object"
|
||||
? JSON.stringify(error.response.data)
|
||||
: String(error.response.data);
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
return `Error: Bad request. ${body}`;
|
||||
case 401:
|
||||
return "Error: Unauthorized. Check your LEEXI_KEY_ID and LEEXI_KEY_SECRET.";
|
||||
case 403:
|
||||
return "Error: Forbidden. Your API key may lack the required permissions.";
|
||||
case 404:
|
||||
return "Error: Resource not found. Check the UUID is correct.";
|
||||
case 429:
|
||||
return "Error: Rate limit exceeded. Wait before making more requests.";
|
||||
default:
|
||||
return `Error: Leexi API returned status ${status}. ${body}`;
|
||||
}
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
return "Error: Request to Leexi timed out. Try again.";
|
||||
} else if (error.code === "ECONNREFUSED") {
|
||||
return "Error: Could not connect to Leexi API. Check your network.";
|
||||
}
|
||||
}
|
||||
|
||||
return `Error: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { apiGet, handleApiError } from "../services/leexi-client.js";
|
||||
import {
|
||||
ListCallsInputSchema,
|
||||
GetCallInputSchema,
|
||||
type ListCallsInput,
|
||||
type GetCallInput,
|
||||
} from "../schemas/input-schemas.js";
|
||||
import type {
|
||||
PaginatedResponse,
|
||||
LeexiCallListItem,
|
||||
LeexiCallDetail,
|
||||
LeexiCallDetailResponse,
|
||||
LeexiSpeaker,
|
||||
LeexiChapter,
|
||||
LeexiTask,
|
||||
LeexiPrompt,
|
||||
LeexiCallTopic,
|
||||
LeexiTranscriptSegment,
|
||||
} from "../types.js";
|
||||
import { ResponseFormat } from "../types.js";
|
||||
import { CHARACTER_LIMIT } from "../constants.js";
|
||||
|
||||
// ─── Formatting helpers ──────────────────────────────────────────────────────
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.round(seconds % 60);
|
||||
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
||||
}
|
||||
|
||||
function formatTimestamp(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.round(seconds % 60);
|
||||
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function flagSpeakerQuality(speaker: LeexiSpeaker): string | null {
|
||||
// Flag speakers that look like room/device names rather than real people
|
||||
if (!speaker.is_user && !speaker.email_address && !speaker.phone_number) {
|
||||
return "possible room/device name — may represent multiple people";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatCallListItemMarkdown(call: LeexiCallListItem): string {
|
||||
const lines: string[] = [
|
||||
`### ${call.title || "(untitled)"}`,
|
||||
`- **Date**: ${call.performed_at}`,
|
||||
`- **Duration**: ${formatDuration(call.duration)}`,
|
||||
`- **Owner**: ${call.owner.name} (${call.owner.email})`,
|
||||
`- **Direction**: ${call.direction} | **Source**: ${call.source}`,
|
||||
`- **UUID**: \`${call.uuid}\``,
|
||||
];
|
||||
|
||||
if (call.participating_users.length > 1) {
|
||||
const others = call.participating_users
|
||||
.filter((u) => u.uuid !== call.owner.uuid)
|
||||
.map((u) => `${u.name} (${u.email})`)
|
||||
.join(", ");
|
||||
if (others) lines.push(`- **Other participants**: ${others}`);
|
||||
}
|
||||
|
||||
if (call.prompts.length > 0) {
|
||||
const summary = call.prompts.find((p) => p.category === "summary");
|
||||
if (summary && summary.completions.length > 0) {
|
||||
const text = summary.completions[0];
|
||||
lines.push(`- **Leexi Summary**: ${text.length > 200 ? text.slice(0, 200) + "..." : text}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (call.tasks.length > 0) {
|
||||
lines.push(`- **Tasks**: ${call.tasks.length} action item(s)`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatCallDetailMarkdown(call: LeexiCallDetail): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// Header
|
||||
sections.push(
|
||||
`# ${call.title || "(untitled)"}`,
|
||||
"",
|
||||
`| Field | Value |`,
|
||||
`|-------|-------|`,
|
||||
`| Date | ${call.performed_at} |`,
|
||||
`| Duration | ${formatDuration(call.duration)} |`,
|
||||
`| Owner | ${call.owner.name} (${call.owner.email}) |`,
|
||||
`| Direction | ${call.direction} |`,
|
||||
`| Source | ${call.source} |`,
|
||||
`| Locale | ${call.locale} |`,
|
||||
`| UUID | \`${call.uuid}\` |`,
|
||||
`| Leexi URL | ${call.leexi_url} |`,
|
||||
""
|
||||
);
|
||||
|
||||
if (call.description) {
|
||||
sections.push(`**Description**: ${call.description}`, "");
|
||||
}
|
||||
|
||||
// Chapters
|
||||
if (call.chapters.length > 0) {
|
||||
sections.push("## Chapters", "");
|
||||
for (const ch of sortChapters(call.chapters)) {
|
||||
sections.push(`### ${ch.title} (${formatTimestamp(ch.start_time)})`, "", ch.text, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks
|
||||
if (call.tasks.length > 0) {
|
||||
sections.push("## Action Items", "");
|
||||
for (const task of call.tasks) {
|
||||
sections.push(formatTaskMarkdown(task));
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
// Topics
|
||||
if (call.call_topics.length > 0) {
|
||||
sections.push("## Topics Discussed", "");
|
||||
for (const topic of call.call_topics) {
|
||||
sections.push(formatTopicMarkdown(topic, call.speakers));
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
// Speakers
|
||||
sections.push("## Speakers", "");
|
||||
for (const speaker of call.speakers) {
|
||||
sections.push(formatSpeakerMarkdown(speaker));
|
||||
}
|
||||
sections.push("");
|
||||
|
||||
// Leexi AI Summaries (labeled with provenance)
|
||||
if (call.prompts.length > 0) {
|
||||
sections.push(
|
||||
"## Leexi AI Summaries",
|
||||
"",
|
||||
"_Note: These summaries are generated by Leexi's AI and may be imprecise. " +
|
||||
"Prefer the chapters and transcript for accurate information._",
|
||||
""
|
||||
);
|
||||
for (const prompt of call.prompts) {
|
||||
sections.push(formatPromptMarkdown(prompt));
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
// Transcript
|
||||
if (call.transcript && call.transcript.length > 0) {
|
||||
sections.push("## Transcript", "");
|
||||
const transcriptText = formatTranscriptMarkdown(call.transcript, call.speakers);
|
||||
sections.push(transcriptText);
|
||||
} else if (call.simple_transcript) {
|
||||
sections.push("## Transcript", "", call.simple_transcript);
|
||||
}
|
||||
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
function sortChapters(chapters: LeexiChapter[]): LeexiChapter[] {
|
||||
return [...chapters].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
|
||||
function formatTaskMarkdown(task: LeexiTask): string {
|
||||
const checkbox = task.done ? "[x]" : "[ ]";
|
||||
const assignee = task.speaker ? ` (${task.speaker.name})` : "";
|
||||
return `- ${checkbox} **${task.subject}**${assignee}${task.description ? `: ${task.description}` : ""}`;
|
||||
}
|
||||
|
||||
function formatTopicMarkdown(
|
||||
topic: LeexiCallTopic,
|
||||
speakers: LeexiSpeaker[]
|
||||
): string {
|
||||
const speaker = topic.speaker
|
||||
? topic.speaker.name
|
||||
: speakers[0]?.name ?? "Unknown";
|
||||
return `- **${topic.topic_name}** — "${topic.keyphrase}" (${formatTimestamp(topic.start_time)}–${formatTimestamp(topic.end_time)}, ${speaker})`;
|
||||
}
|
||||
|
||||
function formatSpeakerMarkdown(speaker: LeexiSpeaker): string {
|
||||
const lines: string[] = [
|
||||
`### ${speaker.name}${speaker.is_user ? " (internal)" : " (external)"}`,
|
||||
`- Talk time: ${formatDuration(speaker.duration)} | Longest monologue: ${formatDuration(speaker.longest_monologue)}`,
|
||||
];
|
||||
if (speaker.email_address) lines.push(`- Email: ${speaker.email_address}`);
|
||||
if (speaker.phone_number) lines.push(`- Phone: ${speaker.phone_number}`);
|
||||
|
||||
const flag = flagSpeakerQuality(speaker);
|
||||
if (flag) {
|
||||
lines.push(`- **Data quality note**: ${flag}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatPromptMarkdown(prompt: LeexiPrompt): string {
|
||||
const lines = [`### ${prompt.title} (${prompt.category})`];
|
||||
for (const completion of prompt.completions) {
|
||||
lines.push("", completion);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatTranscriptMarkdown(
|
||||
segments: LeexiTranscriptSegment[],
|
||||
speakers: LeexiSpeaker[]
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
for (const seg of segments) {
|
||||
const speakerName =
|
||||
speakers[seg.speaker_index]?.name ?? `Speaker ${seg.speaker_index}`;
|
||||
const text = seg.items.map((w) => w.content).join(" ");
|
||||
lines.push(
|
||||
`**${speakerName}** (${formatTimestamp(seg.start_time)}–${formatTimestamp(seg.end_time)}):`,
|
||||
text,
|
||||
""
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ─── Registration ────────────────────────────────────────────────────────────
|
||||
|
||||
export function registerCallTools(server: McpServer): void {
|
||||
// ── leexi_list_calls ───────────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"leexi_list_calls",
|
||||
{
|
||||
title: "List Leexi Calls & Meetings",
|
||||
description: `List calls and meetings in the Leexi workspace with filtering options.
|
||||
|
||||
Supports filtering by date range, owner, source integration, participant, customer email, and customer phone number. Returns call metadata, speakers, chapters summary, AI summaries, and task count. Use leexi_get_call with a UUID for full transcript and details.
|
||||
|
||||
Args:
|
||||
- page (number): Page number, 1-based (default: 1)
|
||||
- items (number): Results per page, 1–100 (default: 20)
|
||||
- order (string): Sort order (default: 'performed_at desc')
|
||||
- date_filter (string): Which date field from/to apply to (default: 'performed_at')
|
||||
- from (string, optional): Start date (ISO 8601)
|
||||
- to (string, optional): End date (ISO 8601)
|
||||
- source (string, optional): Integration slug (e.g. 'aircall', 'gmeet_bot')
|
||||
- owner_uuid (string[], optional): Filter by owner UUID(s)
|
||||
- participating_user_uuid (string[], optional): Filter by participant UUID(s)
|
||||
- customer_email_address (string[], optional): Filter by customer email(s)
|
||||
- customer_phone_number (string[], optional): Filter by customer phone(s)
|
||||
- with_simple_transcript (boolean): Include paragraph-level transcript (default: false)
|
||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
||||
|
||||
Returns paginated list of calls with metadata, speakers, chapters, AI summaries, and tasks.`,
|
||||
inputSchema: ListCallsInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: ListCallsInput) => {
|
||||
try {
|
||||
const queryParams: Record<string, unknown> = {
|
||||
page: params.page,
|
||||
items: params.items,
|
||||
order: params.order,
|
||||
date_filter: params.date_filter,
|
||||
with_simple_transcript: params.with_simple_transcript,
|
||||
};
|
||||
if (params.from) queryParams.from = params.from;
|
||||
if (params.to) queryParams.to = params.to;
|
||||
if (params.source) queryParams.source = params.source;
|
||||
if (params.owner_uuid) queryParams["owner_uuid[]"] = params.owner_uuid;
|
||||
if (params.participating_user_uuid)
|
||||
queryParams["participating_user_uuid[]"] = params.participating_user_uuid;
|
||||
if (params.customer_email_address)
|
||||
queryParams["customer_email_address[]"] = params.customer_email_address;
|
||||
if (params.customer_phone_number)
|
||||
queryParams["customer_phone_number[]"] = params.customer_phone_number;
|
||||
|
||||
const data = await apiGet<PaginatedResponse<LeexiCallListItem>>(
|
||||
"/calls",
|
||||
queryParams
|
||||
);
|
||||
|
||||
const output = {
|
||||
calls: data.data,
|
||||
pagination: data.pagination,
|
||||
};
|
||||
|
||||
let text: string;
|
||||
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||
const { pagination } = data;
|
||||
const lines = [
|
||||
`# Leexi Calls (page ${pagination.page}, ${data.data.length}/${pagination.count})`,
|
||||
"",
|
||||
];
|
||||
if (data.data.length === 0) {
|
||||
lines.push("_No calls found matching the filters._");
|
||||
}
|
||||
for (const call of data.data) {
|
||||
lines.push(formatCallListItemMarkdown(call), "");
|
||||
}
|
||||
if (pagination.page * pagination.items < pagination.count) {
|
||||
lines.push(
|
||||
`_More results available — request page ${pagination.page + 1}_`
|
||||
);
|
||||
}
|
||||
text = lines.join("\n");
|
||||
} else {
|
||||
text = JSON.stringify(output, null, 2);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: output,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: handleApiError(error) }],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ── leexi_get_call ────────────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"leexi_get_call",
|
||||
{
|
||||
title: "Get Leexi Call Details",
|
||||
description: `Get full details of a single call or meeting by UUID. Returns chapters, action items, topics, speakers, Leexi AI summaries, and the full word-level transcript.
|
||||
|
||||
Data quality notes:
|
||||
- Leexi AI summaries are machine-generated and may be imprecise — prefer chapters and transcript.
|
||||
- External speakers without email/phone may be room or device names representing multiple people.
|
||||
- Multi-language calls may have transcript artifacts.
|
||||
|
||||
The markdown output prioritizes: Chapters > Action Items > Topics > Speakers > AI Summaries > Transcript.
|
||||
|
||||
Args:
|
||||
- uuid (string): Call UUID (get from leexi_list_calls)
|
||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
||||
|
||||
Returns full call details including transcript.`,
|
||||
inputSchema: GetCallInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: GetCallInput) => {
|
||||
try {
|
||||
const data = await apiGet<LeexiCallDetailResponse>(
|
||||
`/calls/${params.uuid}`
|
||||
);
|
||||
|
||||
const call = data.data;
|
||||
|
||||
let text: string;
|
||||
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||
text = formatCallDetailMarkdown(call);
|
||||
|
||||
// Truncate if too long
|
||||
if (text.length > CHARACTER_LIMIT) {
|
||||
// Try to keep everything except transcript
|
||||
const withoutTranscript = text.split("## Transcript")[0];
|
||||
if (withoutTranscript && withoutTranscript.length < CHARACTER_LIMIT) {
|
||||
text =
|
||||
withoutTranscript +
|
||||
`## Transcript\n\n_Transcript truncated (${text.length} chars exceeded ${CHARACTER_LIMIT} limit). ` +
|
||||
`Request with response_format='json' for the full transcript data._`;
|
||||
} else {
|
||||
text =
|
||||
text.slice(0, CHARACTER_LIMIT) +
|
||||
`\n\n_Response truncated at ${CHARACTER_LIMIT} characters. ` +
|
||||
`Request with response_format='json' for complete data._`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
text = JSON.stringify(call, null, 2);
|
||||
if (text.length > CHARACTER_LIMIT) {
|
||||
// Return without transcript to stay within limit
|
||||
const { transcript, ...rest } = call;
|
||||
const reduced = {
|
||||
...rest,
|
||||
transcript_truncated: true,
|
||||
transcript_segment_count: transcript?.length ?? 0,
|
||||
note: `Full transcript (${transcript?.length ?? 0} segments) omitted to stay within character limit. Fetch the recording or use the Leexi UI for the full transcript.`,
|
||||
};
|
||||
text = JSON.stringify(reduced, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: handleApiError(error) }],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { apiGet, handleApiError } from "../services/leexi-client.js";
|
||||
import {
|
||||
ListMeetingEventsInputSchema,
|
||||
GetMeetingEventInputSchema,
|
||||
type ListMeetingEventsInput,
|
||||
type GetMeetingEventInput,
|
||||
} from "../schemas/input-schemas.js";
|
||||
import type {
|
||||
PaginatedResponse,
|
||||
LeexiMeetingEventListItem,
|
||||
LeexiMeetingEventDetailResponse,
|
||||
LeexiMeetingEventDetail,
|
||||
} from "../types.js";
|
||||
import { ResponseFormat } from "../types.js";
|
||||
|
||||
// ─── Formatting helpers ──────────────────────────────────────────────────────
|
||||
|
||||
function formatMeetingEventListMarkdown(event: LeexiMeetingEventListItem): string {
|
||||
const lines: string[] = [
|
||||
`### ${event.title || "(untitled)"}`,
|
||||
`- **When**: ${event.start_time} → ${event.end_time}`,
|
||||
`- **Provider**: ${event.meeting_provider ?? "unknown"}`,
|
||||
`- **UUID**: \`${event.uuid}\``,
|
||||
];
|
||||
|
||||
if (event.organizer) {
|
||||
lines.push(`- **Organizer**: ${event.organizer.email}`);
|
||||
}
|
||||
if (event.attendees.length > 0) {
|
||||
lines.push(
|
||||
`- **Attendees**: ${event.attendees.map((a) => a.email).join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
const flags: string[] = [];
|
||||
if (event.to_record) flags.push("to_record");
|
||||
if (event.bot_scheduled) flags.push("bot_scheduled");
|
||||
if (event.bot_running) flags.push("bot_running");
|
||||
if (event.internal) flags.push("internal");
|
||||
if (flags.length > 0) lines.push(`- **Flags**: ${flags.join(", ")}`);
|
||||
|
||||
if (event.bot_runs.length > 0) {
|
||||
lines.push(`- **Bot runs**: ${event.bot_runs.length}`);
|
||||
}
|
||||
|
||||
lines.push(`- **Origin**: ${event.origin} | **Active**: ${event.active}`);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatMeetingEventDetailMarkdown(event: LeexiMeetingEventDetail): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
sections.push(
|
||||
`# ${event.title || "(untitled)"}`,
|
||||
"",
|
||||
`| Field | Value |`,
|
||||
`|-------|-------|`,
|
||||
`| When | ${event.start_time} → ${event.end_time} |`,
|
||||
`| Provider | ${event.meeting_provider ?? "unknown"} |`,
|
||||
`| Meeting URL | ${event.meeting_url} |`,
|
||||
`| UUID | \`${event.uuid}\` |`,
|
||||
`| Origin | ${event.origin} |`,
|
||||
`| Internal | ${event.internal} |`,
|
||||
`| Direction | ${event.direction ?? "—"} |`,
|
||||
`| To record | ${event.to_record} |`,
|
||||
`| Bot scheduled | ${event.bot_scheduled} |`,
|
||||
`| Bot running | ${event.bot_running} |`,
|
||||
`| Active | ${event.active} |`,
|
||||
""
|
||||
);
|
||||
|
||||
if (event.description) {
|
||||
sections.push(`**Description**: ${event.description}`, "");
|
||||
}
|
||||
|
||||
if (event.organizer) {
|
||||
sections.push(`**Organizer**: ${event.organizer.email}`, "");
|
||||
}
|
||||
|
||||
if (event.attendees.length > 0) {
|
||||
sections.push(
|
||||
"## Attendees",
|
||||
"",
|
||||
...event.attendees.map((a) => `- ${a.email}`),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
if (event.bot_runs.length > 0) {
|
||||
sections.push("## Bot Runs", "");
|
||||
for (const run of event.bot_runs) {
|
||||
sections.push(
|
||||
`- **${run.uuid}**: ${run.start_time} → ${run.end_time}` +
|
||||
(run.recording_start_time
|
||||
? ` (recording started ${run.recording_start_time})`
|
||||
: "") +
|
||||
(run.end_reason ? ` — ended: ${run.end_reason}` : "")
|
||||
);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
if (event.integration_user) {
|
||||
const iu = event.integration_user;
|
||||
sections.push(
|
||||
"## Integration",
|
||||
"",
|
||||
`- **User**: ${iu.name} (${iu.email})`,
|
||||
`- **Integration**: ${iu.integration.name} (${iu.integration.slug})`,
|
||||
""
|
||||
);
|
||||
|
||||
if (iu.calls.length > 0) {
|
||||
sections.push("### Associated Calls", "");
|
||||
for (const call of iu.calls) {
|
||||
sections.push(
|
||||
`- \`${call.uuid}\` — ${call.performed_at} (${call.source}, ${Math.round(call.duration)}s)` +
|
||||
(call.leexi_url ? ` [View](${call.leexi_url})` : "")
|
||||
);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
// ─── Registration ────────────────────────────────────────────────────────────
|
||||
|
||||
export function registerMeetingTools(server: McpServer): void {
|
||||
// ── leexi_list_meeting_events ──────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"leexi_list_meeting_events",
|
||||
{
|
||||
title: "List Leexi Meeting Events",
|
||||
description: `List scheduled meeting events in the Leexi workspace. Meeting events represent calendar entries that may or may not have been recorded. Use leexi_get_meeting_event for details and associated call recordings.
|
||||
|
||||
Args:
|
||||
- page (number): Page number, 1-based (default: 1)
|
||||
- items (number): Results per page, 1–100 (default: 20)
|
||||
- order (string): Sort order (default: 'start_time desc')
|
||||
- created_by (string, optional): Filter by origin — 'calendar', 'manual', or 'api'
|
||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
||||
|
||||
Returns paginated list of meeting events.`,
|
||||
inputSchema: ListMeetingEventsInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: ListMeetingEventsInput) => {
|
||||
try {
|
||||
const queryParams: Record<string, unknown> = {
|
||||
page: params.page,
|
||||
items: params.items,
|
||||
order: params.order,
|
||||
};
|
||||
if (params.created_by) queryParams.created_by = params.created_by;
|
||||
|
||||
const data = await apiGet<PaginatedResponse<LeexiMeetingEventListItem>>(
|
||||
"/meeting_events",
|
||||
queryParams
|
||||
);
|
||||
|
||||
const output = {
|
||||
meeting_events: data.data,
|
||||
pagination: data.pagination,
|
||||
};
|
||||
|
||||
let text: string;
|
||||
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||
const { pagination } = data;
|
||||
const lines = [
|
||||
`# Leexi Meeting Events (page ${pagination.page}, ${data.data.length}/${pagination.count})`,
|
||||
"",
|
||||
];
|
||||
if (data.data.length === 0) {
|
||||
lines.push("_No meeting events found._");
|
||||
}
|
||||
for (const event of data.data) {
|
||||
lines.push(formatMeetingEventListMarkdown(event), "");
|
||||
}
|
||||
if (pagination.page * pagination.items < pagination.count) {
|
||||
lines.push(
|
||||
`_More results available — request page ${pagination.page + 1}_`
|
||||
);
|
||||
}
|
||||
text = lines.join("\n");
|
||||
} else {
|
||||
text = JSON.stringify(output, null, 2);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: output,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: handleApiError(error) }],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ── leexi_get_meeting_event ───────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"leexi_get_meeting_event",
|
||||
{
|
||||
title: "Get Leexi Meeting Event Details",
|
||||
description: `Get full details of a single meeting event by UUID. Returns meeting metadata, attendees, bot run history, integration info, and links to associated call recordings. Use the call UUIDs from the response with leexi_get_call for transcripts and notes.
|
||||
|
||||
Args:
|
||||
- uuid (string): Meeting event UUID (get from leexi_list_meeting_events)
|
||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
||||
|
||||
Returns meeting event details with associated call links.`,
|
||||
inputSchema: GetMeetingEventInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: GetMeetingEventInput) => {
|
||||
try {
|
||||
const data = await apiGet<LeexiMeetingEventDetailResponse>(
|
||||
`/meeting_events/${params.uuid}`
|
||||
);
|
||||
|
||||
const event = data.data;
|
||||
|
||||
let text: string;
|
||||
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||
text = formatMeetingEventDetailMarkdown(event);
|
||||
} else {
|
||||
text = JSON.stringify(event, null, 2);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: handleApiError(error) }],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { apiGet, handleApiError } from "../services/leexi-client.js";
|
||||
import {
|
||||
ListUsersInputSchema,
|
||||
ListTeamsInputSchema,
|
||||
type ListUsersInput,
|
||||
type ListTeamsInput,
|
||||
} from "../schemas/input-schemas.js";
|
||||
import type {
|
||||
PaginatedResponse,
|
||||
LeexiUser,
|
||||
LeexiTeam,
|
||||
} from "../types.js";
|
||||
import { ResponseFormat } from "../types.js";
|
||||
|
||||
// ─── Formatting helpers ──────────────────────────────────────────────────────
|
||||
|
||||
function formatUserMarkdown(user: LeexiUser): string {
|
||||
const lines: string[] = [
|
||||
`### ${user.name}`,
|
||||
`- **Email**: ${user.email}`,
|
||||
`- **UUID**: \`${user.uuid}\``,
|
||||
];
|
||||
if (user.team) {
|
||||
lines.push(`- **Team**: ${user.team.name}${user.team.active ? "" : " (inactive)"}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatTeamMarkdown(team: LeexiTeam): string {
|
||||
return `### ${team.name}\n- **UUID**: \`${team.uuid}\`\n- **Active**: ${team.active}`;
|
||||
}
|
||||
|
||||
// ─── Registration ────────────────────────────────────────────────────────────
|
||||
|
||||
export function registerUserTools(server: McpServer): void {
|
||||
// ── leexi_list_users ────────────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"leexi_list_users",
|
||||
{
|
||||
title: "List Leexi Users",
|
||||
description: `List all users in the Leexi workspace. Returns user name, email, UUID, and team assignment. Use UUIDs from this endpoint as owner_uuid or participating_user_uuid filters in leexi_list_calls.
|
||||
|
||||
Args:
|
||||
- page (number): Page number, 1-based (default: 1)
|
||||
- items (number): Results per page, 1–100 (default: 20)
|
||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
||||
|
||||
Returns paginated list of users with name, email, UUID, and team info.`,
|
||||
inputSchema: ListUsersInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: ListUsersInput) => {
|
||||
try {
|
||||
const data = await apiGet<PaginatedResponse<LeexiUser>>("/users", {
|
||||
page: params.page,
|
||||
items: params.items,
|
||||
});
|
||||
|
||||
const output = {
|
||||
users: data.data,
|
||||
pagination: data.pagination,
|
||||
};
|
||||
|
||||
let text: string;
|
||||
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||
const { pagination } = data;
|
||||
const lines = [
|
||||
`# Leexi Users (page ${pagination.page}, ${data.data.length}/${pagination.count})`,
|
||||
"",
|
||||
];
|
||||
for (const user of data.data) {
|
||||
lines.push(formatUserMarkdown(user), "");
|
||||
}
|
||||
if (pagination.page * pagination.items < pagination.count) {
|
||||
lines.push(
|
||||
`_More results available — request page ${pagination.page + 1}_`
|
||||
);
|
||||
}
|
||||
text = lines.join("\n");
|
||||
} else {
|
||||
text = JSON.stringify(output, null, 2);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: output,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: handleApiError(error) }],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ── leexi_list_teams ───────────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"leexi_list_teams",
|
||||
{
|
||||
title: "List Leexi Teams",
|
||||
description: `List all teams in the Leexi workspace. Returns team name, UUID, and active status.
|
||||
|
||||
Args:
|
||||
- page (number): Page number, 1-based (default: 1)
|
||||
- items (number): Results per page, 1–100 (default: 20)
|
||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
||||
|
||||
Returns paginated list of teams.`,
|
||||
inputSchema: ListTeamsInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: ListTeamsInput) => {
|
||||
try {
|
||||
const data = await apiGet<PaginatedResponse<LeexiTeam>>("/teams", {
|
||||
page: params.page,
|
||||
items: params.items,
|
||||
});
|
||||
|
||||
const output = {
|
||||
teams: data.data,
|
||||
pagination: data.pagination,
|
||||
};
|
||||
|
||||
let text: string;
|
||||
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||
const { pagination } = data;
|
||||
const lines = [
|
||||
`# Leexi Teams (page ${pagination.page}, ${data.data.length}/${pagination.count})`,
|
||||
"",
|
||||
];
|
||||
for (const team of data.data) {
|
||||
lines.push(formatTeamMarkdown(team), "");
|
||||
}
|
||||
text = lines.join("\n");
|
||||
} else {
|
||||
text = JSON.stringify(output, null, 2);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: output,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: handleApiError(error) }],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
// ─── Pagination ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiPagination {
|
||||
page: number;
|
||||
items: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
pagination: LeexiPagination;
|
||||
}
|
||||
|
||||
// ─── Users & Teams ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiTeam {
|
||||
uuid: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface LeexiUser {
|
||||
uuid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
team: LeexiTeam | null;
|
||||
}
|
||||
|
||||
// ─── Speakers ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiSpeaker {
|
||||
uuid: string;
|
||||
name: string;
|
||||
index: number;
|
||||
is_user: boolean;
|
||||
email_address: string | null;
|
||||
phone_number: string | null;
|
||||
duration: number;
|
||||
longest_monologue: number;
|
||||
}
|
||||
|
||||
// ─── Chapters ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiChapter {
|
||||
uuid: string;
|
||||
index: number;
|
||||
title: string;
|
||||
text: string;
|
||||
start_time: number;
|
||||
}
|
||||
|
||||
// ─── Prompts (AI-generated summaries) ────────────────────────────────────────
|
||||
|
||||
export interface LeexiPrompt {
|
||||
uuid: string;
|
||||
category: string;
|
||||
title: string;
|
||||
completions: string[];
|
||||
}
|
||||
|
||||
// ─── Tasks ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiTaskEditor {
|
||||
uuid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface LeexiTask {
|
||||
uuid: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
done: boolean;
|
||||
active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_editor: LeexiTaskEditor | null;
|
||||
speaker: LeexiSpeaker | null;
|
||||
}
|
||||
|
||||
// ─── Call Topics ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiCallTopic {
|
||||
uuid: string;
|
||||
topic_name: string;
|
||||
keyphrase: string;
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
speaker: Pick<LeexiSpeaker, "uuid" | "name" | "index" | "is_user" | "email_address" | "phone_number"> | null;
|
||||
}
|
||||
|
||||
// ─── Transcript ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiTranscriptWord {
|
||||
content: string;
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
}
|
||||
|
||||
export interface LeexiTranscriptSegment {
|
||||
speaker_index: number;
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
items: LeexiTranscriptWord[];
|
||||
}
|
||||
|
||||
// ─── Conversation Type ───────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiConversationType {
|
||||
uuid: string;
|
||||
slug: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
// ─── Owner / Participant ─────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiUserRef {
|
||||
uuid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
// ─── Call (list response) ────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiCallListItem {
|
||||
uuid: string;
|
||||
title: string;
|
||||
description: string;
|
||||
source: string;
|
||||
source_id: string;
|
||||
direction: string;
|
||||
duration: number;
|
||||
is_video: boolean;
|
||||
locale: string;
|
||||
visible: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
performed_at: string;
|
||||
leexi_url: string;
|
||||
recording_url: string | null;
|
||||
transcript_url: string | null;
|
||||
audio_archived_at: string | null;
|
||||
video_archived_at: string | null;
|
||||
transcript_archived_at: string | null;
|
||||
completions_archived_at: string | null;
|
||||
owner: LeexiUserRef;
|
||||
participating_users: LeexiUserRef[];
|
||||
speakers: LeexiSpeaker[];
|
||||
chapters: LeexiChapter[];
|
||||
prompts: LeexiPrompt[];
|
||||
tasks: LeexiTask[];
|
||||
conversation_type: LeexiConversationType | null;
|
||||
customer_email_addresses: string[];
|
||||
customer_phone_numbers: string[];
|
||||
deal: unknown;
|
||||
feedbacks: unknown[];
|
||||
scorecards: unknown[];
|
||||
meeting_event: unknown;
|
||||
simple_transcript?: string;
|
||||
}
|
||||
|
||||
// ─── Call (detail response — extends list item) ──────────────────────────────
|
||||
|
||||
export interface LeexiCallDetail extends LeexiCallListItem {
|
||||
call_topics: LeexiCallTopic[];
|
||||
transcript: LeexiTranscriptSegment[];
|
||||
}
|
||||
|
||||
export interface LeexiCallDetailResponse {
|
||||
data: LeexiCallDetail;
|
||||
}
|
||||
|
||||
// ─── Meeting Events ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface LeexiBotRun {
|
||||
uuid: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
recording_start_time: string | null;
|
||||
end_reason: string | null;
|
||||
}
|
||||
|
||||
export interface LeexiMeetingEventListItem {
|
||||
uuid: string;
|
||||
title: string;
|
||||
organizer: { email: string } | null;
|
||||
attendees: { email: string }[];
|
||||
meeting_url: string;
|
||||
meeting_provider: string | null;
|
||||
internal: boolean;
|
||||
direction: string | null;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
description: string | null;
|
||||
owned: boolean;
|
||||
to_record: boolean;
|
||||
bot_scheduled: boolean;
|
||||
bot_running: boolean;
|
||||
bot_runs: LeexiBotRun[];
|
||||
origin: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
recording_notified: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface LeexiMeetingEventCallRef {
|
||||
uuid: string;
|
||||
source: string;
|
||||
source_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
performed_at: string;
|
||||
locale: string;
|
||||
duration: number;
|
||||
direction: string;
|
||||
is_video: boolean;
|
||||
visible: boolean;
|
||||
recording_url: string | null;
|
||||
transcript_url: string | null;
|
||||
leexi_url: string;
|
||||
}
|
||||
|
||||
export interface LeexiIntegrationUser {
|
||||
uuid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
active: boolean;
|
||||
user: LeexiUserRef;
|
||||
integration: {
|
||||
active: boolean;
|
||||
category: string;
|
||||
logo: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
uuid: string;
|
||||
};
|
||||
calls: LeexiMeetingEventCallRef[];
|
||||
}
|
||||
|
||||
export interface LeexiMeetingEventDetail extends LeexiMeetingEventListItem {
|
||||
integration_user: LeexiIntegrationUser | null;
|
||||
}
|
||||
|
||||
export interface LeexiMeetingEventDetailResponse {
|
||||
success: boolean;
|
||||
data: LeexiMeetingEventDetail;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ─── Response format enum ────────────────────────────────────────────────────
|
||||
|
||||
export enum ResponseFormat {
|
||||
MARKDOWN = "markdown",
|
||||
JSON = "json",
|
||||
}
|
||||
Reference in New Issue
Block a user