# Directory Structure
```
├── .env.example
├── .gitignore
├── .gitlab-ci.yml
├── Dockerfile
├── package-lock.json
├── package.json
├── README.MD
├── scripts
│ └── build.js
├── src
│ ├── api
│ │ ├── schemas.ts
│ │ ├── tldv-api.ts
│ │ └── types.ts
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | TLDV_API_KEY=dummy
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | dist
3 | DS_Store
4 | .env
5 |
```
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | stages:
2 | - build
3 | - release
4 | - deploy
5 |
6 | variables:
7 | DOCKER_REGISTRY: $CI_REGISTRY
8 | DOCKER_IMAGE: $CI_REGISTRY_IMAGE
9 |
10 | release-version:
11 | stage: release
12 | image: node:22
13 | script:
14 | - git config --global user.email "${GIT_CI_EMAIL}"
15 | - git config --global user.name "${GIT_CI_USER}"
16 | - npm i standard-version --location=global
17 | - npm run release -- --no-verify
18 | - git remote set-url --push origin https://$GIT_CI_USER:[email protected]/$CI_PROJECT_PATH.git
19 | - git push --follow-tags origin HEAD:$CI_COMMIT_REF_NAME
20 | rules:
21 | - if: $CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TITLE !~ /^chore\(release\).*\ \[release\]$/
22 |
23 | build:
24 | stage: build
25 | image: node:22
26 | script:
27 | - npm ci
28 | - npm run build
29 | artifacts:
30 | paths:
31 | - dist/
32 |
33 | publish-npm:
34 | stage: deploy
35 | image: node:22
36 | script:
37 | - npm config set @tldx:registry=https://gitlab.com/api/v4/packages/npm/
38 | - npm config set -- '//gitlab.com/api/v4/packages/npm/:_authToken'="${CI_JOB_TOKEN}"
39 | - npm config set -- '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken'="${CI_JOB_TOKEN}"
40 | - npm ci
41 | - npm run build
42 | - npm publish
43 | - echo "-- npm publish completed successfully"
44 | rules:
45 | - if: $CI_COMMIT_TAG && $CI_COMMIT_TAG =~ /^v?[0-9]+\.[0-9]+\.[0-9]+$/
46 |
47 | publish-docker:
48 | stage: deploy
49 | image: docker:latest
50 | services:
51 | - docker:dind
52 | before_script:
53 | - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
54 | script:
55 | - docker build -t $DOCKER_IMAGE:$CI_COMMIT_TAG .
56 | - docker tag $DOCKER_IMAGE:$CI_COMMIT_TAG $DOCKER_IMAGE:latest
57 | - docker push $DOCKER_IMAGE:$CI_COMMIT_TAG
58 | - docker push $DOCKER_IMAGE:latest
59 | - echo "-- docker publish completed successfully"
60 | rules:
61 | - if: $CI_COMMIT_TAG && $CI_COMMIT_TAG =~ /^v?[0-9]+\.[0-9]+\.[0-9]+$/
62 |
63 |
```
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
```markdown
1 | # Official MCP Server for tl;dv API
2 |
3 | 🚀 **The First and Only MCP Server for Google Meet, Zoom, and Microsoft Teams Integration**
4 |
5 | This project provides a Model Context Protocol (MCP) server enabling seamless interaction with the [tl;dv](https://tldv.io/) API. As the pioneering MCP solution for video conferencing platforms, it unlocks the power of tl;dv's meeting intelligence across Google Meet, Zoom, and Microsoft Teams through a standardized interface. This integration allows AI models and MCP clients to access, analyze, and derive insights from your meetings across all major platforms in one unified way.
6 |
7 | ## Features
8 |
9 | * **List Meetings:** Retrieve meetings based on filters (query, date range, participation status, type) across all supported platforms.
10 | * **Get Meeting Metadata:** Fetch detailed information for a specific meeting by ID, regardless of the platform it was hosted on.
11 | * **Get Transcript:** Obtain the transcript for any meeting ID, with consistent formatting across all platforms.
12 | * **Get Highlights:** Retrieve AI-generated highlights for meetings from any supported platform.
13 | * **Import Meeting (Coming Soon):** Functionality to import meetings via URL from any supported platform.
14 |
15 | ## Prerequisites
16 |
17 | * **tl;dv Account:** A Business or Enterprise tl;dv account is required.
18 | * **tl;dv API Key:** You need an API key, which can be requested from your tl;dv settings: [https://tldv.io/app/settings/personal-settings/api-keys](https://tldv.io/app/settings/personal-settings/api-keys).
19 | * **Node.js & npm (for Node installation):** If installing via Node.js, ensure Node.js and npm are installed.
20 | * **Docker (for Docker installation):** If installing via Docker, ensure Docker is installed and running.
21 |
22 | ## Installation and Configuration
23 |
24 | You can run this MCP server using either Docker or Node.js. Configure your MCP client (e.g., Claude Desktop, Cursor) to connect to the server.
25 |
26 | ### Using Docker
27 |
28 | Go in the repo.
29 |
30 | 1. **Build the Docker image:**
31 | ```bash
32 | docker build -t tldv-mcp-server .
33 | ```
34 |
35 | 2. **Configure your MCP Client:**
36 | Update your MCP client's configuration file (e.g., `claude_desktop_config.json`). The exact location and format may vary depending on the client.
37 |
38 | ```json
39 | {
40 | "mcpServers": {
41 | "tldv": {
42 | "command": "docker",
43 | "args": [
44 | "run",
45 |
46 | "--rm",
47 | "--init",
48 | "-e",
49 | "TLDV_API_KEY=<your-tldv-api-key>",
50 | "tldv-mcp-server"
51 | ],
52 | }
53 | }
54 | }
55 | ```
56 | Replace `<your-tldv-api-key>` with your actual tl;dv API key.
57 |
58 | ### Using Node.js
59 |
60 | 1. **Install dependencies:**
61 | ```bash
62 | npm install
63 | ```
64 |
65 | 2. **Build the server:**
66 | ```bash
67 | npm run build
68 | ```
69 | This command creates a `dist` folder containing the compiled server code (`index.js`).
70 |
71 | 3. **Configure your MCP Client:**
72 | Update your MCP client's configuration file.
73 |
74 | ```json
75 | {
76 | "mcpServers": {
77 | "tldv": {
78 | "command": "node",
79 | "args": ["/absolute/path/to/tldv-mcp-server/dist/index.js"],
80 | "env": {
81 | "TLDV_API_KEY": "your_tldv_api_key"
82 | }
83 | }
84 | }
85 | }
86 | ```
87 | Replace `/absolute/path/to/tldv-mcp-server/dist/index.js` with the correct absolute path to the built server file and `your_tldv_api_key` with your tl;dv API key.
88 |
89 | *Refer to your specific MCP client's documentation for detailed setup instructions (e.g., [Claude Tools](https://modelcontextprotocol.io/quickstart/user)).*
90 |
91 | *Disclaimer* Once you are updating this config file, you will need to kill your MCP client and restart it for the changes to be effective.
92 |
93 | ## Development
94 |
95 | 1. **Install dependencies:**
96 | ```bash
97 | npm install
98 | ```
99 |
100 | 2. **Set up Environment Variables:**
101 | Copy the example environment file:
102 | ```bash
103 | cp .env.example .env
104 | ```
105 | Edit the `.env` file and add your `TLDV_API_KEY`. Other variables can be configured as needed.
106 |
107 |
108 | 3. **Run in development mode:**
109 | This command starts the server with auto-reloading on file changes:
110 | ```bash
111 | npm run watch
112 | ```
113 |
114 | 4. **Update client for local development:**
115 | Configure your MCP client to use the local development server path (typically `/path/to/your/project/dist/index.js`). Ensure the `TLDV_API_KEY` is accessible, either through the client's `env` configuration or loaded via the `.env` file by the server process.
116 |
117 | 5. **Reload your MCP Client**
118 | Since you are running the watch command, it will recompiled a new version. Reloading your Client (e.g Claud Desktop App), your changes will be effective.
119 |
120 | ## Debugging
121 |
122 | * **Console Logs:** Check the console output when running `npm run dev` for detailed logs. The server uses the `debug` library; you can control log levels via environment variables (e.g., `DEBUG=tldv-mcp:*`).
123 | * **Node.js Debugger:** Utilize standard Node.js debugging tools (e.g., Chrome DevTools Inspector, VS Code debugger) by launching the server process with the appropriate flags (e.g., `node --inspect dist/index.js`).
124 | * **MCP Client Logs:** Check the logs provided by your MCP client, which might show the requests sent and responses received from this server.
125 |
126 | ## Learn More
127 |
128 | * [tl;dv Developer API Documentation](https://doc.tldv.io/)
129 | * [Model Context Protocol (MCP) Specification](https://modelcontextprotocol.io/introduction)
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "sourceMap": false,
5 | "inlineSources": false,
6 | "removeComments": false,
7 | "target": "ES2022",
8 | "module": "NodeNext",
9 | "declaration": true,
10 | "outDir": "./dist",
11 | "emitDecoratorMetadata": true,
12 | "experimentalDecorators": true,
13 | "esModuleInterop": true,
14 | "resolveJsonModule": true,
15 | "paths": {
16 | "~/*": ["src/*"]
17 | }
18 | }
19 | }
20 |
```
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | const { build } = require('esbuild');
4 |
5 | async function bundle() {
6 | try {
7 | await build({
8 | entryPoints: ['src/index.ts'],
9 | bundle: true,
10 | platform: 'node',
11 | target: 'node22',
12 | outfile: 'dist/index.js',
13 | sourcemap: true,
14 | minify: true,
15 | format: 'cjs',
16 | banner: {
17 | js: '#!/usr/bin/env node',
18 | },
19 | // No external packages, include everything in the bundle
20 | external: [],
21 | });
22 | console.log('Bundle complete! Output: dist/index.js');
23 | } catch (error) {
24 | console.error('Bundle failed:', error);
25 | process.exit(1);
26 | }
27 | }
28 |
29 | bundle();
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "tldv-mcp",
3 | "version": "1.0.0",
4 | "description": "TLDR API MCP Server",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "start": "node dist/index.js",
9 | "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
10 | "build": "rm -rf dist && npm run bundle && chmod +x dist/*.js",
11 | "bundle": "node scripts/build.js",
12 | "prepare": "npm run build",
13 | "watch": "tsc --watch",
14 | "release": "standard-version -m \"chore(release): {{currentTag}} [release]\""
15 | },
16 | "dependencies": {
17 | "@modelcontextprotocol/sdk": "^1.8.0",
18 | "axios": "^1.8.4",
19 | "dotenv": "^16.3.1",
20 | "fastify": "^4.26.1",
21 | "zod": "^3.22.4"
22 | },
23 | "devDependencies": {
24 | "@types/node": "^20.11.19",
25 | "esbuild": "^0.25.2",
26 | "esbuild-node-externals": "^1.18.0",
27 | "ts-node-dev": "^2.0.0",
28 | "typescript": "^5.3.3"
29 | },
30 | "volta": {
31 | "node": "22.11.0"
32 | }
33 | }
34 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Stage 1: Build the application
2 | FROM node:22-slim AS builder
3 |
4 | WORKDIR /app
5 |
6 | # Copy package files and install dependencies
7 | COPY package.json /app
8 | COPY package-lock.json /app
9 | COPY tsconfig.json /app
10 |
11 | COPY src /app/src
12 | # Use npm ci for cleaner installs, ensure package-lock.json exists
13 | RUN npm install
14 |
15 | # Build the TypeScript code
16 | RUN npm run build
17 |
18 | # Prune development dependencies
19 | RUN npm prune --production
20 |
21 | # Stage 2: Create the final production image
22 | FROM node:22-slim
23 |
24 | WORKDIR /app
25 |
26 | # Create a non-root user
27 | RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
28 |
29 | # Copy built code and production dependencies from the builder stage
30 | COPY --from=builder /app/dist ./dist
31 | COPY --from=builder /app/node_modules ./node_modules
32 | COPY package.json ./
33 |
34 | # Set ownership to the non-root user
35 | RUN chown -R appuser:appgroup /app
36 |
37 | # Switch to the non-root user
38 | USER appuser
39 |
40 | # Command to run the server
41 | # The API key will be passed via environment variable at runtime
42 | CMD ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/src/api/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface TldvConfig {
2 | apiKey: string;
3 | baseUrl?: string;
4 | }
5 |
6 | export interface TldvResponse<T> {
7 | data: T;
8 | error?: string;
9 | }
10 |
11 | export interface User {
12 | name: string;
13 | email: string;
14 | }
15 |
16 | export interface Template {
17 | id: string;
18 | label: string;
19 | }
20 |
21 | export interface Meeting {
22 | id: string;
23 | name: string;
24 | happenedAt: string;
25 | url: string;
26 | organizer: User;
27 | invitees: User[];
28 | template: Template;
29 | }
30 |
31 | export interface Sentence {
32 | speaker: string;
33 | text: string;
34 | startTime: number;
35 | endTime: number;
36 | }
37 |
38 | export interface HighlightTopic {
39 | title: string;
40 | summary: string;
41 | }
42 |
43 | export interface Highlight {
44 | text: string;
45 | startTime: number;
46 | source: 'manual' | 'auto';
47 | topic: HighlightTopic;
48 | }
49 |
50 | export interface GetTranscriptResponse {
51 | id: string;
52 | meetingId: string;
53 | data: Sentence[];
54 | }
55 |
56 | export interface GetHighlightsResponse {
57 | meetingId: string;
58 | data: Highlight[];
59 | }
60 |
61 | export interface ImportMeetingParams {
62 | name: string;
63 | url: string;
64 | happenedAt?: string;
65 | dryRun?: boolean;
66 | }
67 |
68 | export interface ImportMeetingResponse {
69 | success: boolean;
70 | jobId: string;
71 | message: string;
72 | }
73 |
74 | export interface GetMeetingsParams {
75 | query?: string;
76 | page?: number;
77 | limit?: number;
78 | from?: string;
79 | to?: string;
80 | onlyParticipated?: boolean;
81 | meetingType?: 'internal' | 'external';
82 | }
83 |
84 | export interface GetMeetingsResponse {
85 | page: number;
86 | pages: number;
87 | total: number;
88 | pageSize: number;
89 | results: Meeting[];
90 | }
91 |
92 | export interface HealthResponse {
93 | status: string;
94 | }
95 |
96 | export interface ValidationError {
97 | property: string;
98 | constraints: Record<string, string>;
99 | }
100 |
101 | export interface ValidationErrorResponse {
102 | message: string;
103 | error: {
104 | message: string;
105 | errors: ValidationError[];
106 | }[];
107 | }
108 |
109 | export interface BasicErrorResponse {
110 | name: string;
111 | message: string;
112 | }
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { GetMeetingsParamsSchema } from "./api/schemas";
2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import { TldvApi } from "./api/tldv-api";
5 | import dotenv from "dotenv";
6 | import z from "zod";
7 |
8 | dotenv.config();
9 |
10 | async function main() {
11 | console.info({ name: "Initializing TLDV API..." });
12 | const tldvApi = new TldvApi({
13 | apiKey: process.env.TLDV_API_KEY,
14 | });
15 |
16 | console.info({ name: "Starting MCP server..." });
17 |
18 | const tools = {
19 | "get-meeting-metadata": {
20 | name: "get-meeting-metadata",
21 | description: "Get a meeting by its ID. The meeting ID is a unique identifier for a meeting. It will return the meeting metadata, including the name, the date, the organizer, participants and more.",
22 | inputSchema: z.object({ id: z.string() }),
23 | },
24 | "get-transcript": {
25 | name: "get-transcript",
26 | description: "Get transcript by meeting ID. The transcript is a list of messages exchanged between the participants in the meeting. It's time-stamped and contains the speaker and the message",
27 | inputSchema: z.object({ meetingId: z.string() }),
28 | },
29 | "list-meetings": {
30 | name: "list-meetings",
31 | description: "List all meetings based on the filters provided. You can filter by date, status, and more. Those meetings are the sames you have access to in the TLDV app.",
32 | inputSchema: GetMeetingsParamsSchema,
33 | },
34 | "get-highlights": {
35 | name: "get-highlights",
36 | description: "Allows you to get highlights from a meeting by providing a meeting ID.",
37 | inputSchema: z.object({ meetingId: z.string() }),
38 | },
39 | };
40 |
41 | const server = new McpServer({
42 | name: "tldv-server",
43 | version: "1.0.0",
44 | }, {
45 | capabilities: {
46 | logging: {
47 | level: "debug",
48 | },
49 | prompts: {},
50 | tools: tools,
51 | resources: {},
52 | },
53 | instructions: "You are a helpful assistant that can help with TLDV API requests.",
54 | });
55 |
56 | //Register tool handlers
57 | server.tool(
58 | tools["get-meeting-metadata"].name,
59 | tools["get-meeting-metadata"].description,
60 | tools["get-meeting-metadata"].inputSchema.shape,
61 | async ({ id }) => {
62 | const meeting = await tldvApi.getMeeting(id);
63 | return {
64 | content: [{ type: "text", text: JSON.stringify(meeting) }]
65 | };
66 | }
67 | );
68 |
69 | server.tool(
70 | tools["get-transcript"].name,
71 | tools["get-transcript"].description,
72 | tools["get-transcript"].inputSchema.shape,
73 | async ({ meetingId }) => {
74 | const transcript = await tldvApi.getTranscript(meetingId);
75 | return {
76 | content: [{ type: "text", text: JSON.stringify(transcript) }]
77 | };
78 | }
79 | );
80 |
81 | server.tool(
82 | tools["list-meetings"].name,
83 | tools["list-meetings"].description,
84 | tools["list-meetings"].inputSchema.shape,
85 | async (input) => {
86 | const meetings = await tldvApi.getMeetings(input);
87 | return {
88 | content: [{ type: "text", text: JSON.stringify(meetings) }]
89 | };
90 | }
91 | );
92 |
93 | server.tool(
94 | tools["get-highlights"].name,
95 | tools["get-highlights"].description,
96 | tools["get-highlights"].inputSchema.shape,
97 | async ({ meetingId }) => {
98 | const highlights = await tldvApi.getHighlights(meetingId);
99 | return {
100 | content: [{ type: "text", text: JSON.stringify(highlights) }]
101 | };
102 | }
103 | );
104 |
105 | console.info({ message: "Initializing StdioServerTransport..." });
106 | const transport = new StdioServerTransport();
107 |
108 | console.info({ message: "Connecting to MCP server..." });
109 | await server.connect(transport);
110 | }
111 |
112 | main().catch((error) => {
113 | console.error({ message: "Fatal error in main():", error });
114 | // process.exit(1);
115 | });
116 |
```
--------------------------------------------------------------------------------
/src/api/schemas.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod';
2 |
3 | /**
4 | * Configuration for the TLDR API client
5 | */
6 | export const TldvConfigSchema = z.object({
7 | apiKey: z.string().min(1, 'API key is required')
8 | });
9 |
10 | export type TldvConfig = z.infer<typeof TldvConfigSchema>;
11 |
12 | /**
13 | * Generic response wrapper for all API calls
14 | */
15 | export const TldvResponseSchema = <T extends z.ZodType>(dataSchema: T) => dataSchema.nullable()
16 |
17 | export type TldvResponse<T> = z.infer<ReturnType<typeof TldvResponseSchema<z.ZodType<T>>>>;
18 |
19 | /**
20 | * User information schema
21 | */
22 | export const UserSchema = z.object({
23 | name: z.string(),
24 | email: z.string().email(),
25 | });
26 |
27 | export type User = z.infer<typeof UserSchema>;
28 |
29 | /**
30 | * Template information schema
31 | */
32 | export const TemplateSchema = z.object({
33 | id: z.string(),
34 | label: z.string(),
35 | });
36 |
37 | export type Template = z.infer<typeof TemplateSchema>;
38 |
39 | /**
40 | * Meeting information schema
41 | */
42 | export const MeetingSchema = z.object({
43 | id: z.string(),
44 | name: z.string(),
45 | happenedAt: z.string().datetime(),
46 | url: z.string().url(),
47 | organizer: UserSchema,
48 | invitees: z.array(UserSchema),
49 | template: TemplateSchema,
50 | });
51 |
52 | export type Meeting = z.infer<typeof MeetingSchema>;
53 |
54 | /**
55 | * Sentence information schema for transcripts
56 | */
57 | export const SentenceSchema = z.object({
58 | speaker: z.string(),
59 | text: z.string(),
60 | startTime: z.number().int().nonnegative(),
61 | endTime: z.number().int().nonnegative(),
62 | });
63 |
64 | export type Sentence = z.infer<typeof SentenceSchema>;
65 |
66 | /**
67 | * Highlight topic information schema
68 | */
69 | export const HighlightTopicSchema = z.object({
70 | title: z.string(),
71 | summary: z.string(),
72 | });
73 |
74 | export type HighlightTopic = z.infer<typeof HighlightTopicSchema>;
75 |
76 | /**
77 | * Highlight information schema
78 | */
79 | export const HighlightSchema = z.object({
80 | text: z.string(),
81 | startTime: z.number().int().nonnegative(),
82 | source: z.enum(['manual', 'auto']),
83 | topic: HighlightTopicSchema,
84 | });
85 |
86 | export type Highlight = z.infer<typeof HighlightSchema>;
87 |
88 | /**
89 | * Transcript response schema
90 | */
91 | export const GetTranscriptResponseSchema = z.object({
92 | id: z.string(),
93 | meetingId: z.string(),
94 | data: z.array(SentenceSchema),
95 | });
96 |
97 | export type GetTranscriptResponse = z.infer<typeof GetTranscriptResponseSchema>;
98 |
99 | /**
100 | * Highlights response schema
101 | */
102 | export const GetHighlightsResponseSchema = z.object({
103 | meetingId: z.string(),
104 | data: z.array(HighlightSchema),
105 | });
106 |
107 | export type GetHighlightsResponse = z.infer<typeof GetHighlightsResponseSchema>;
108 |
109 | /**
110 | * Import meeting response schema
111 | */
112 | export const ImportMeetingResponseSchema = z.object({
113 | success: z.boolean(),
114 | jobId: z.string(),
115 | message: z.string(),
116 | });
117 |
118 | export type ImportMeetingResponse = z.infer<typeof ImportMeetingResponseSchema>;
119 |
120 | /**
121 | * Get meetings parameters schema
122 | */
123 | export const GetMeetingsParamsSchema = z.object({
124 | query: z.string().optional(), // search query
125 | page: z.number().int().positive().optional(), // page number
126 | limit: z.number().int().positive().optional().default(50), // number of results per page
127 | from: z.string().datetime().optional(), // start date
128 | to: z.string().datetime().optional(), // end date
129 | onlyParticipated: z.boolean().optional(), // only return meetings where the user participated
130 |
131 | // meeting type. internal is default.
132 | // This is used to filter meetings by type. Type is determined by comparing the organizer's email with the invitees' emails.
133 | // If the organizer's domain is different from at least one of the invitees' domains, the meeting is external.
134 | // Otherwise, the meeting is internal.
135 | meetingType: z.enum(['internal', 'external']).optional(),
136 | });
137 |
138 | export type GetMeetingsParams = z.infer<typeof GetMeetingsParamsSchema>;
139 |
140 | /**
141 | * Get meetings response schema
142 | */
143 | export const GetMeetingsResponseSchema = z.object({
144 | page: z.number().int().nonnegative(), // current page number
145 | pages: z.number().int().positive(), // total number of pages
146 | total: z.number().int().nonnegative(), // total number of results
147 | pageSize: z.number().int().positive(), // number of results per page
148 | results: z.array(MeetingSchema), // array of meetings
149 | });
150 |
151 | export type GetMeetingsResponse = z.infer<typeof GetMeetingsResponseSchema>;
152 |
153 | /**
154 | * Health check response schema
155 | */
156 | export const HealthResponseSchema = z.object({
157 | status: z.string(),
158 | });
159 |
160 | export type HealthResponse = z.infer<typeof HealthResponseSchema>;
161 |
162 | /**
163 | * Validation error schema
164 | */
165 | export const ValidationErrorSchema = z.object({
166 | property: z.string(),
167 | constraints: z.record(z.string()),
168 | });
169 |
170 | export type ValidationError = z.infer<typeof ValidationErrorSchema>;
171 |
172 | /**
173 | * Validation error response schema
174 | */
175 | export const ValidationErrorResponseSchema = z.object({
176 | message: z.string(),
177 | error: z.array(
178 | z.object({
179 | message: z.string(),
180 | errors: z.array(ValidationErrorSchema),
181 | })
182 | ),
183 | });
184 |
185 | export type ValidationErrorResponse = z.infer<typeof ValidationErrorResponseSchema>;
186 |
187 | /**
188 | * Basic error response schema
189 | */
190 | export const BasicErrorResponseSchema = z.object({
191 | name: z.string(),
192 | message: z.string(),
193 | });
194 |
195 | export type BasicErrorResponse = z.infer<typeof BasicErrorResponseSchema>;
```
--------------------------------------------------------------------------------
/src/api/tldv-api.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | GetHighlightsResponse,
3 | GetMeetingsParams,
4 | GetMeetingsParamsSchema,
5 | GetMeetingsResponse,
6 | GetTranscriptResponse,
7 | HealthResponse,
8 | Meeting,
9 | TldvConfig,
10 | TldvConfigSchema,
11 | } from './schemas';
12 |
13 | import { TldvResponse } from './types';
14 | import axios from 'axios';
15 |
16 | const BASE_URL = 'https://pasta.tldv.io/v1alpha1';
17 |
18 | const MAX_RETRIES = 3;
19 | const RETRY_DELAY = 1_000;
20 | const MAX_RETRY_DELAY = 2_000;
21 |
22 | /**
23 | * TLDV API Client
24 | *
25 | * This class provides a type-safe interface to interact with the TLDV API.
26 | * It handles authentication, request formatting, and response validation.
27 | *
28 | * @example
29 | * ```typescript
30 | * const api = new TldvApi({
31 | * apiKey: 'your-api-key'
32 | * });
33 | *
34 | * ```typescript
35 | * const meeting = await tldvApi.getMeeting(id);
36 | * ```
37 | */
38 | export class TldvApi {
39 | private apiKey: string;
40 | private baseUrl: string;
41 | private headers: any;
42 |
43 | /**
44 | * Creates a new instance of the TLDV API client
45 | * @param config - Configuration object containing API key and optional base URL
46 | * @throws {Error} If the configuration is invalid
47 | */
48 | constructor(config: TldvConfig) {
49 | const validatedConfig = TldvConfigSchema.parse(config);
50 | this.apiKey = validatedConfig.apiKey;
51 | this.baseUrl = BASE_URL;
52 | this.headers = {
53 | 'x-api-key': this.apiKey,
54 | 'Content-Type': 'application/json',
55 | };
56 | }
57 |
58 | /**
59 | * Makes a request to the TLDV API
60 | * @param endpoint - The API endpoint to call
61 | * @param options - Request options including method, body, etc.
62 | * @returns A promise that resolves to the validated API response
63 | */
64 | private async request<T>(
65 | endpoint: string,
66 | options: RequestInit = {},
67 | retryCount = 0,
68 | maxRetries = MAX_RETRIES
69 | ): Promise<TldvResponse<T>> {
70 | try {
71 | const response = await axios(`${this.baseUrl}${endpoint}`, {
72 | ...options,
73 | headers: {
74 | ...this.headers,
75 | },
76 | });
77 |
78 | if (response.status > 200) {
79 | throw new Error(response.data.message || 'API request failed');
80 | }
81 |
82 | // Ensure the response is properly formatted
83 | const responseData = response.data;
84 |
85 | // If the response is already in the expected format, return it
86 | if (responseData && typeof responseData === 'object' && 'data' in responseData) {
87 | return responseData as TldvResponse<T>;
88 | }
89 |
90 | // Otherwise, wrap it in the expected format
91 | return {
92 | data: responseData as T,
93 | error: undefined,
94 | };
95 | } catch (error) {
96 | // Determine if we should retry based on the error
97 | const shouldRetry =
98 | retryCount < maxRetries &&
99 | (
100 | // Network errors
101 | (error instanceof Error && error.message.includes('Network Error')) ||
102 | // 5xx server errors
103 | (axios.isAxiosError(error) && error.response && error.response.status >= 500) ||
104 | // Rate limiting
105 | (axios.isAxiosError(error) && error.response && error.response.status === 429)
106 | );
107 |
108 | if (shouldRetry) {
109 | // Calculate exponential backoff delay: 2^retryCount * 1000ms (1s, 2s, 4s, etc.)
110 | const delay = Math.min(RETRY_DELAY * 2 ** retryCount, MAX_RETRY_DELAY);
111 |
112 | // Wait before retrying
113 | await new Promise(resolve => setTimeout(resolve, delay));
114 |
115 | // Retry the request
116 | return this.request<T>(endpoint, options, retryCount + 1, maxRetries);
117 | }
118 |
119 | return {
120 | data: null as T,
121 | error: error instanceof Error ? error.message : 'Unknown error occurred',
122 | };
123 | }
124 | }
125 |
126 |
127 | /**
128 | * Retrieves a meeting by its ID
129 | *
130 | * @param meetingId - The unique identifier of the meeting
131 | * @returns A promise that resolves to the meeting details
132 | *
133 | * @example
134 | * ```typescript
135 | * const meeting = await api.getMeeting('meeting-123');
136 | * ```
137 | */
138 | async getMeeting(meetingId: string): Promise<TldvResponse<Meeting>> {
139 | return this.request<Meeting>(`/meetings/${meetingId}`);
140 | }
141 |
142 | /**
143 | * Retrieves a list of meetings with optional filtering
144 | *
145 | * @param params - Optional parameters for filtering and pagination
146 | * @returns A promise that resolves to the paginated list of meetings
147 | *
148 | * @example
149 | * ```typescript
150 | * const meetings = await api.getMeetings({
151 | * query: 'team sync',
152 | * page: 1,
153 | * limit: 10,
154 | * from: '2024-01-01T00:00:00Z',
155 | * to: '2024-12-31T23:59:59Z',
156 | * onlyParticipated: true,
157 | * meetingType: 'internal'
158 | * });
159 | * ```
160 | */
161 | async getMeetings(params: GetMeetingsParams = {}): Promise<TldvResponse<GetMeetingsResponse>> {
162 | const validatedParams = GetMeetingsParamsSchema.parse(params);
163 | const queryParams = new URLSearchParams();
164 |
165 | if (validatedParams.query) queryParams.append('query', validatedParams.query);
166 | if (validatedParams.page) queryParams.append('page', validatedParams.page.toString());
167 | if (validatedParams.limit) queryParams.append('limit', validatedParams.limit.toString());
168 | if (validatedParams.from) queryParams.append('from', validatedParams.from);
169 | if (validatedParams.to) queryParams.append('to', validatedParams.to);
170 | if (validatedParams.onlyParticipated !== undefined) queryParams.append('onlyParticipated', validatedParams.onlyParticipated.toString());
171 | if (validatedParams.meetingType) queryParams.append('meetingType', validatedParams.meetingType);
172 |
173 | return this.request<GetMeetingsResponse>(`/meetings?${queryParams}`);
174 | }
175 |
176 | /**
177 | * Retrieves the transcript for a specific meeting
178 | *
179 | * @param meetingId - The unique identifier of the meeting
180 | * @returns A promise that resolves to the meeting transcript
181 | *
182 | * @example
183 | * ```typescript
184 | * const transcript = await api.getTranscript('meeting-123');
185 | * ```
186 | */
187 | async getTranscript(meetingId: string): Promise<TldvResponse<GetTranscriptResponse>> {
188 | return this.request<GetTranscriptResponse>(`/meetings/${meetingId}/transcript`);
189 | }
190 |
191 | /**
192 | * Retrieves the highlights for a specific meeting
193 | *
194 | * @param meetingId - The unique identifier of the meeting
195 | * @returns A promise that resolves to the meeting highlights
196 | *
197 | * @example
198 | * ```typescript
199 | * const highlights = await api.getHighlights('meeting-123');
200 | * ```
201 | */
202 | async getHighlights(meetingId: string): Promise<TldvResponse<GetHighlightsResponse>> {
203 | return this.request<GetHighlightsResponse>(`/meetings/${meetingId}/highlights`);
204 | }
205 |
206 | /**
207 | * Checks the health status of the API
208 | *
209 | * @returns A promise that resolves to the health status
210 | *
211 | * @example
212 | * ```typescript
213 | * const health = await api.healthCheck();
214 | * ```
215 | */
216 | async healthCheck(): Promise<TldvResponse<HealthResponse>> {
217 | return this.request<HealthResponse>('/health');
218 | }
219 | }
```