@speakai/mcp-server 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +70 -9
  2. package/dist/index.js +83 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <p align="center">
8
8
  Connect Claude, Cursor, Windsurf, and other AI assistants to your <a href="https://speakai.co">Speak AI</a> workspace.<br/>
9
- 81 tools, 5 resources, 3 prompts, 26 CLI commands — transcribe, analyze, search, and manage media at scale.
9
+ 82 tools, 5 resources, 3 prompts, 28 CLI commands — transcribe, analyze, search, and manage media at scale.
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -160,10 +160,10 @@ SPEAK_API_KEY=your-key npx @speakai/mcp-server
160
160
 
161
161
  ---
162
162
 
163
- ## MCP Tools (81)
163
+ ## MCP Tools (82)
164
164
 
165
165
  <details>
166
- <summary>Media (14 tools)</summary>
166
+ <summary>Media (15 tools)</summary>
167
167
 
168
168
  | Tool | Description |
169
169
  |---|---|
@@ -181,6 +181,7 @@ SPEAK_API_KEY=your-key npx @speakai/mcp-server
181
181
  | `delete_media` | Permanently delete a media file |
182
182
  | `toggle_media_favorite` | Mark or unmark media as a favorite |
183
183
  | `reanalyze_media` | Re-run AI analysis with latest models |
184
+ | `bulk_move_media` | Move multiple media files to a folder in one call |
184
185
 
185
186
  </details>
186
187
 
@@ -331,7 +332,7 @@ SPEAK_API_KEY=your-key npx @speakai/mcp-server
331
332
 
332
333
  | Tool | Description |
333
334
  |---|---|
334
- | `export_media` | Export as PDF, DOCX, SRT, VTT, TXT, CSV, or Markdown |
335
+ | `export_media` | Export as PDF, DOCX, SRT, VTT, TXT, or CSV |
335
336
  | `export_multiple_media` | Batch export with optional merge into one file |
336
337
 
337
338
  </details>
@@ -407,7 +408,7 @@ Parameters: days (optional, default: 7), folder (optional)
407
408
 
408
409
  ---
409
410
 
410
- ## CLI (26 Commands)
411
+ ## CLI (28 Commands)
411
412
 
412
413
  Install globally and configure once:
413
414
 
@@ -436,12 +437,12 @@ npx @speakai/mcp-server config set-key
436
437
 
437
438
  | Command | Description |
438
439
  |---|---|
439
- | `list-media` / `ls` | List media files with filtering and pagination |
440
+ | `list-media` / `ls` | List media files with filtering, date ranges, and pagination |
440
441
  | `upload <source>` | Upload media from URL or local file (`--wait` to poll) |
441
442
  | `get-transcript` / `transcript <id>` | Get transcript (`--plain` or `--json`) |
442
443
  | `get-insights` / `insights <id>` | Get AI insights (topics, sentiment, keywords) |
443
444
  | `status <id>` | Check media processing status |
444
- | `export <id>` | Export transcript (`-f pdf\|docx\|srt\|vtt\|txt\|csv\|md`) |
445
+ | `export <id>` | Export transcript (`-f pdf\|docx\|srt\|vtt\|txt\|csv`) |
445
446
  | `update <id>` | Update media metadata (name, description, tags, folder) |
446
447
  | `delete <id>` | Delete a media file |
447
448
  | `favorites <id>` | Toggle favorite status |
@@ -461,6 +462,7 @@ npx @speakai/mcp-server config set-key
461
462
  | Command | Description |
462
463
  |---|---|
463
464
  | `list-folders` / `folders` | List all folders |
465
+ | `move <folderId> <mediaIds...>` | Move media files to a folder |
464
466
  | `create-folder <name>` | Create a new folder |
465
467
  | `clips` | List clips (filter by media or folder) |
466
468
  | `clip <mediaId>` | Create a clip (`--start` and `--end` in seconds) |
@@ -515,6 +517,12 @@ speakai-mcp schedule-meeting "https://zoom.us/j/123456" -t "Weekly Standup"
515
517
 
516
518
  # List videos as JSON for scripting
517
519
  speakai-mcp ls --type video --json | jq '.mediaList[].name'
520
+
521
+ # List media from the last week
522
+ speakai-mcp ls --from 2026-03-19 --to 2026-03-26
523
+
524
+ # Move 3 files to a folder
525
+ speakai-mcp move folder123 media1 media2 media3
518
526
  ```
519
527
 
520
528
  ---
@@ -589,14 +597,67 @@ AI: -> list_media(from: "2026-03-18", mediaType: "audio")
589
597
 
590
598
  ### Authentication
591
599
 
592
- All requests require `x-speakai-key` (API key) and `x-access-token` (JWT) headers. The MCP server handles token management automatically. Access tokens expire the client refreshes them via `POST /v1/auth/accessToken`.
600
+ The MCP server and CLI handle token management automatically. If you're calling the REST API directly, here's the full auth flow:
601
+
602
+ **Step 1 — Get an access token:**
603
+
604
+ ```bash
605
+ curl -X POST https://api.speakai.co/v1/auth/accessToken \
606
+ -H "Content-Type: application/json" \
607
+ -H "x-speakai-key: YOUR_API_KEY"
608
+ ```
609
+
610
+ Response:
611
+ ```json
612
+ {
613
+ "data": {
614
+ "email": "you@example.com",
615
+ "accessToken": "eyJhbG...",
616
+ "refreshToken": "eyJhbG..."
617
+ }
618
+ }
619
+ ```
620
+
621
+ **Step 2 — Use the token on all subsequent requests:**
622
+
623
+ ```bash
624
+ curl https://api.speakai.co/v1/media \
625
+ -H "x-speakai-key: YOUR_API_KEY" \
626
+ -H "x-access-token: ACCESS_TOKEN_FROM_STEP_1"
627
+ ```
628
+
629
+ **Step 3 — Refresh before expiry:**
630
+
631
+ ```bash
632
+ curl -X POST https://api.speakai.co/v1/auth/refreshToken \
633
+ -H "Content-Type: application/json" \
634
+ -H "x-speakai-key: YOUR_API_KEY" \
635
+ -H "x-access-token: CURRENT_ACCESS_TOKEN" \
636
+ -d '{"refreshToken": "REFRESH_TOKEN_FROM_STEP_1"}'
637
+ ```
638
+
639
+ **Token Lifetimes:**
640
+
641
+ | Token | Expiry | How to Renew |
642
+ |---|---|---|
643
+ | Access token | 80 minutes | Refresh endpoint or re-authenticate |
644
+ | Refresh token | 24 hours | Re-authenticate with API key |
645
+
646
+ **Auth Rate Limits:** 5 requests per 60 seconds on both `/v1/auth/accessToken` and `/v1/auth/refreshToken`.
647
+
648
+ ### Data Model Notes
649
+
650
+ - **Folder IDs:** Folders have both `_id` (MongoDB ObjectId) and `folderId` (string). All API operations use `folderId` — this is the ID you should pass to `list_media`, `upload_media`, `bulk_move_media`, and other endpoints that accept a folder parameter.
651
+ - **Media IDs:** Media items use `mediaId` (returned in list responses as `_id`).
593
652
 
594
653
  ### Rate Limits
595
654
 
596
- - Implement exponential backoff on `429` responses
655
+ - The MCP client automatically retries on `429` with exponential backoff
656
+ - For direct API usage, implement exponential backoff and respect `Retry-After` headers
597
657
  - Cache stable data (folder lists, field definitions, supported languages)
598
658
  - Use `export_multiple_media` over individual exports for batch operations
599
659
  - Use `upload_and_analyze` instead of manual upload + poll + fetch loops
660
+ - Use `bulk_move_media` to move multiple items at once instead of updating one by one
600
661
 
601
662
  ### Error Format
602
663
 
package/dist/index.js CHANGED
@@ -166,6 +166,16 @@ var init_client = __esm({
166
166
  originalRequest.headers["x-access-token"] = accessToken;
167
167
  return speakClient(originalRequest);
168
168
  }
169
+ if (error.response?.status === 429 && retryCount < 3) {
170
+ const retryAfter = error.response.headers["retry-after"];
171
+ const delaySeconds = retryAfter ? parseInt(retryAfter, 10) : Math.pow(2, retryCount + 1);
172
+ const delayMs = (Number.isFinite(delaySeconds) ? delaySeconds : 2) * 1e3;
173
+ process.stderr.write(`[speakai-mcp] Rate limited, retrying in ${delayMs / 1e3}s...
174
+ `);
175
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
176
+ originalRequest._retryCount = retryCount + 1;
177
+ return speakClient(originalRequest);
178
+ }
169
179
  return Promise.reject(error);
170
180
  }
171
181
  );
@@ -1418,6 +1428,29 @@ function register(server, client) {
1418
1428
  }
1419
1429
  }
1420
1430
  );
1431
+ server.tool(
1432
+ "bulk_move_media",
1433
+ "Move multiple media files to a folder in a single operation. Use this for batch reorganization instead of updating media one by one.",
1434
+ {
1435
+ folderId: import_zod.z.string().min(1).describe("Target folder ID to move media into"),
1436
+ mediaIds: import_zod.z.array(import_zod.z.string().min(1)).min(1).describe("Array of media IDs to move")
1437
+ },
1438
+ async (body) => {
1439
+ try {
1440
+ const result = await api.put("/v1/media/move", body);
1441
+ return {
1442
+ content: [
1443
+ { type: "text", text: JSON.stringify(result.data, null, 2) }
1444
+ ]
1445
+ };
1446
+ } catch (err) {
1447
+ return {
1448
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
1449
+ isError: true
1450
+ };
1451
+ }
1452
+ }
1453
+ );
1421
1454
  }
1422
1455
  var import_zod;
1423
1456
  var init_media3 = __esm({
@@ -1562,10 +1595,10 @@ function register3(server, client) {
1562
1595
  const api = client ?? speakClient;
1563
1596
  server.tool(
1564
1597
  "export_media",
1565
- "Export a media file's transcript or insights in various formats (pdf, docx, srt, vtt, txt, csv, md).",
1598
+ "Export a media file's transcript or insights in various formats (pdf, docx, srt, vtt, txt, csv).",
1566
1599
  {
1567
1600
  mediaId: import_zod3.z.string().min(1).describe("Unique identifier of the media file"),
1568
- fileType: import_zod3.z.enum(["pdf", "docx", "srt", "vtt", "txt", "csv", "md"]).describe("Desired export format"),
1601
+ fileType: import_zod3.z.enum(["pdf", "docx", "srt", "vtt", "txt", "csv"]).describe("Desired export format"),
1569
1602
  isSpeakerNames: import_zod3.z.boolean().optional().describe("Include speaker names in export"),
1570
1603
  isSpeakerEmail: import_zod3.z.boolean().optional().describe("Include speaker emails in export"),
1571
1604
  isTimeStamps: import_zod3.z.boolean().optional().describe("Include timestamps in export"),
@@ -1573,12 +1606,11 @@ function register3(server, client) {
1573
1606
  isRedacted: import_zod3.z.boolean().optional().describe("Apply PII redaction to export"),
1574
1607
  redactedCategories: import_zod3.z.array(import_zod3.z.string()).optional().describe("Specific categories to redact")
1575
1608
  },
1576
- async ({ mediaId, fileType, ...query }) => {
1609
+ async ({ mediaId, fileType, ...body }) => {
1577
1610
  try {
1578
1611
  const result = await api.post(
1579
1612
  `/v1/media/export/${mediaId}/${fileType}`,
1580
- null,
1581
- { params: query }
1613
+ body
1582
1614
  );
1583
1615
  return {
1584
1616
  content: [
@@ -1598,7 +1630,7 @@ function register3(server, client) {
1598
1630
  "Export multiple media files at once, optionally merged into a single file.",
1599
1631
  {
1600
1632
  mediaIds: import_zod3.z.array(import_zod3.z.string()).describe("Array of media IDs to export"),
1601
- fileType: import_zod3.z.enum(["pdf", "docx", "srt", "vtt", "txt", "csv", "md"]).describe("Desired export format"),
1633
+ fileType: import_zod3.z.enum(["pdf", "docx", "srt", "vtt", "txt", "csv"]).describe("Desired export format"),
1602
1634
  isSpeakerNames: import_zod3.z.boolean().optional().describe("Include speaker names in export"),
1603
1635
  isSpeakerEmail: import_zod3.z.boolean().optional().describe("Include speaker emails in export"),
1604
1636
  isTimeStamps: import_zod3.z.boolean().optional().describe("Include timestamps in export"),
@@ -3957,7 +3989,7 @@ function createCli() {
3957
3989
  rl.close();
3958
3990
  printSuccess("Setup complete! You're ready to go.");
3959
3991
  });
3960
- program.command("list-media").alias("ls").description("List media files").option("-t, --type <type>", "Filter by type (audio, video, text)").option("-p, --page <n>", "Page number (0-based)", "0").option("-s, --page-size <n>", "Results per page", "20").option("--sort <field>", "Sort field", "createdAt:desc").option("-f, --folder <id>", "Filter by folder ID").option("-n, --name <filter>", "Filter by name").option("--favorites", "Show only favorites").option("--json", "Output raw JSON").action(async (opts) => {
3992
+ program.command("list-media").alias("ls").description("List media files").option("-t, --type <type>", "Filter by type (audio, video, text)").option("-p, --page <n>", "Page number (0-based)", "0").option("-s, --page-size <n>", "Results per page", "20").option("--sort <field>", "Sort field", "createdAt:desc").option("-f, --folder <id>", "Filter by folder ID").option("-n, --name <filter>", "Filter by name").option("--from <date>", "Start date filter (ISO 8601, e.g. 2026-01-01)").option("--to <date>", "End date filter (ISO 8601)").option("--favorites", "Show only favorites").option("--json", "Output raw JSON").action(async (opts) => {
3961
3993
  requireApiKey();
3962
3994
  const client = await getClient();
3963
3995
  try {
@@ -3971,6 +4003,8 @@ function createCli() {
3971
4003
  if (opts.type) params.mediaType = opts.type;
3972
4004
  if (opts.folder) params.folderId = opts.folder;
3973
4005
  if (opts.name) params.filterName = opts.name;
4006
+ if (opts.from) params.from = opts.from;
4007
+ if (opts.to) params.to = opts.to;
3974
4008
  if (opts.favorites) params.isFavorites = true;
3975
4009
  const res = await client.get("/v1/media", { params });
3976
4010
  const data = res.data?.data;
@@ -4164,20 +4198,19 @@ function createCli() {
4164
4198
  });
4165
4199
  program.command("export").description("Export media transcript/insights").argument("<mediaId>", "Media file ID").option(
4166
4200
  "-f, --format <type>",
4167
- "Export format (pdf, docx, srt, vtt, txt, csv, md)",
4201
+ "Export format (pdf, docx, srt, vtt, txt, csv)",
4168
4202
  "txt"
4169
4203
  ).option("--speakers", "Include speaker names").option("--timestamps", "Include timestamps").option("--redacted", "Apply PII redaction").option("--json", "Output raw JSON").action(async (mediaId, opts) => {
4170
4204
  requireApiKey();
4171
4205
  const client = await getClient();
4172
4206
  try {
4173
- const params = {};
4174
- if (opts.speakers) params.isSpeakerNames = true;
4175
- if (opts.timestamps) params.isTimeStamps = true;
4176
- if (opts.redacted) params.isRedacted = true;
4207
+ const body = {};
4208
+ if (opts.speakers) body.isSpeakerNames = true;
4209
+ if (opts.timestamps) body.isTimeStamps = true;
4210
+ if (opts.redacted) body.isRedacted = true;
4177
4211
  const res = await client.post(
4178
4212
  `/v1/media/export/${mediaId}/${opts.format}`,
4179
- null,
4180
- { params }
4213
+ body
4181
4214
  );
4182
4215
  if (opts.json) {
4183
4216
  printJson(res.data);
@@ -4256,8 +4289,8 @@ function createCli() {
4256
4289
  }
4257
4290
  const folders = Array.isArray(data) ? data : data?.folderList ?? data?.folders ?? [];
4258
4291
  printTable(folders, [
4259
- { key: "_id", label: "ID", width: 14 },
4260
- { key: "name", label: "Name", width: 40 },
4292
+ { key: "folderId", label: "Folder ID", width: 20 },
4293
+ { key: "name", label: "Name", width: 34 },
4261
4294
  { key: "createdAt", label: "Created", width: 20 }
4262
4295
  ]);
4263
4296
  } catch (err) {
@@ -4441,6 +4474,22 @@ function createCli() {
4441
4474
  process.exit(1);
4442
4475
  }
4443
4476
  });
4477
+ program.command("move").description("Move one or more media files to a folder").argument("<folderId>", "Target folder ID").argument("<mediaIds...>", "Media file IDs to move").option("--json", "Output raw JSON").action(async (folderId, mediaIds, opts) => {
4478
+ requireApiKey();
4479
+ const client = await getClient();
4480
+ try {
4481
+ const res = await client.put("/v1/media/move", { folderId, mediaIds });
4482
+ const data = res.data?.data;
4483
+ if (opts.json) {
4484
+ printJson(data);
4485
+ } else {
4486
+ printSuccess(`Moved ${mediaIds.length} item(s) to folder ${folderId}`);
4487
+ }
4488
+ } catch (err) {
4489
+ printError(err.response?.data?.message ?? err.message);
4490
+ process.exit(1);
4491
+ }
4492
+ });
4444
4493
  program.command("create-folder").description("Create a new folder").argument("<name>", "Folder name").option("--json", "Output raw JSON").action(async (name, opts) => {
4445
4494
  requireApiKey();
4446
4495
  const client = await getClient();
@@ -4450,7 +4499,7 @@ function createCli() {
4450
4499
  if (opts.json) {
4451
4500
  printJson(data);
4452
4501
  } else {
4453
- printSuccess(`Folder created: ${data?._id ?? "OK"} \u2014 ${name}`);
4502
+ printSuccess(`Folder created: ${data?.folderId ?? data?._id ?? "OK"} \u2014 ${name}`);
4454
4503
  }
4455
4504
  } catch (err) {
4456
4505
  printError(err.response?.data?.message ?? err.message);
@@ -4479,21 +4528,23 @@ function createCli() {
4479
4528
  printJson(data);
4480
4529
  return;
4481
4530
  }
4482
- const total = data?.totalCount ?? data?.total ?? "\u2014";
4483
- const audio = data?.audioCount ?? data?.audio ?? "\u2014";
4484
- const video = data?.videoCount ?? data?.video ?? "\u2014";
4485
- const text = data?.textCount ?? data?.text ?? "\u2014";
4486
- console.log(`Total media: ${total}`);
4487
- console.log(` Audio: ${audio}`);
4488
- console.log(` Video: ${video}`);
4489
- console.log(` Text: ${text}`);
4490
- if (data?.totalDuration) {
4491
- const hrs = Math.round(data.totalDuration / 3600 * 10) / 10;
4492
- console.log(`Duration: ${hrs}h total`);
4493
- }
4494
- if (data?.totalSize) {
4495
- const gb = Math.round(data.totalSize / (1024 * 1024 * 1024) * 100) / 100;
4496
- console.log(`Storage: ${gb} GB`);
4531
+ const total = data?.totalMedia ?? "\u2014";
4532
+ const analyzed = data?.analyzedMedia ?? "\u2014";
4533
+ const notAnalyzed = data?.notAnalyzedMedia ?? "\u2014";
4534
+ console.log(`Total media: ${total}`);
4535
+ console.log(` Analyzed: ${analyzed}`);
4536
+ console.log(` Not analyzed: ${notAnalyzed}`);
4537
+ if (data?.duration) {
4538
+ const hrs = Math.round(data.duration / 3600 * 10) / 10;
4539
+ console.log(`Duration: ${hrs}h total`);
4540
+ }
4541
+ if (data?.analyzedMinutes) {
4542
+ const hrs = Math.round(data.analyzedMinutes / 60 * 10) / 10;
4543
+ console.log(`Analyzed: ${hrs}h (${data.analyzedMinutes} min)`);
4544
+ }
4545
+ if (data?.fileSize) {
4546
+ const gb = Math.round(data.fileSize / (1024 * 1024 * 1024) * 100) / 100;
4547
+ console.log(`Storage: ${gb} GB`);
4497
4548
  }
4498
4549
  } catch (err) {
4499
4550
  printError(err.response?.data?.message ?? err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speakai/mcp-server",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Official Speak AI MCP Server — connect Claude and other AI assistants to Speak AI's transcription, insights, and media management API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",