@speakai/mcp-server 1.8.0 → 1.10.0

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 +8 -4
  2. package/dist/index.js +178 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -242,7 +242,7 @@ For questions about data handling, see [speakai.co/privacy](https://speakai.co/p
242
242
 
243
243
  ## What you can do once installed
244
244
 
245
- Speak AI ships 83 tools your AI assistant can call. You don't memorize them — Claude/ChatGPT pick the right ones based on what you ask. Examples by category:
245
+ Speak AI ships 84 tools your AI assistant can call. You don't memorize them — Claude/ChatGPT pick the right ones based on what you ask. Examples by category:
246
246
 
247
247
  | Ask | Tools used (auto) |
248
248
  |---|---|
@@ -250,6 +250,7 @@ Speak AI ships 83 tools your AI assistant can call. You don't memorize them —
250
250
  | "Summarize this week's meetings into decisions, owners, and risks" | `list_media`, `get_media_insights` |
251
251
  | "Pull action items from yesterday's call" | `get_media_insights`, `ask_magic_prompt` |
252
252
  | "Schedule the AI to join my 2pm Zoom" | `schedule_meeting_event` |
253
+ | "Pull the live transcript from my current MS Teams meeting since last fetch" | `list_meeting_events`, `get_live_meeting_transcript` |
253
254
  | "Find a 30-second webinar highlight and export captions" | `create_clip`, `export_media` |
254
255
  | "Export the transcript as a PDF and captions as SRT" | `export_media` |
255
256
  | "Compare Q1 sales calls against Q2 sales calls and summarize changed objections" | `search_media`, `ask_magic_prompt` |
@@ -271,7 +272,7 @@ Get a Speak AI API key at [app.speakai.co/developers/apikeys](https://app.speaka
271
272
 
272
273
  The `@speakai/mcp-server` npm package provides:
273
274
 
274
- - A CLI (`speakai-mcp`) for scripting and pipelines (28 commands).
275
+ - A CLI (`speakai-mcp`) for scripting and pipelines (30 commands).
275
276
  - A stdio-mode MCP server for clients that don't support remote HTTP transport.
276
277
  - An auto-setup wizard that detects installed MCP clients and configures them.
277
278
 
@@ -526,7 +527,7 @@ SPEAK_API_KEY=your-key npx @speakai/mcp-server
526
527
  </details>
527
528
 
528
529
  <details>
529
- <summary>Meeting Assistant (4 tools)</summary>
530
+ <summary>Meeting Assistant (5 tools)</summary>
530
531
 
531
532
  | Tool | Description |
532
533
  |---|---|
@@ -534,6 +535,7 @@ SPEAK_API_KEY=your-key npx @speakai/mcp-server
534
535
  | `schedule_meeting_event` | Schedule AI assistant to join a meeting |
535
536
  | `remove_assistant_from_meeting` | Remove assistant from active meeting |
536
537
  | `delete_scheduled_assistant` | Cancel a scheduled meeting assistant |
538
+ | `get_live_meeting_transcript` | Pull only the new sentences added to a live (or just-ended) meeting transcript since your previous call. Works on Zoom / Google Meet / MS Teams while the bot is recording. |
537
539
 
538
540
  </details>
539
541
 
@@ -636,7 +638,7 @@ Parameters: days (optional, default: 7), folder (optional)
636
638
 
637
639
  **Example:** "Use the meeting-brief prompt with days=14 to cover the last two weeks"
638
640
 
639
- ### CLI (28 Commands)
641
+ ### CLI (30 Commands)
640
642
 
641
643
  Install globally and configure once:
642
644
 
@@ -701,7 +703,9 @@ npx @speakai/mcp-server config set-key
701
703
  |---|---|
702
704
  | `stats` | Show workspace media statistics |
703
705
  | `languages` | List supported transcription languages |
706
+ | `list-meeting-events` | List scheduled/completed meeting events (`--platform`, `--status`, `--sort`) |
704
707
  | `schedule-meeting <url>` | Schedule AI assistant to join a meeting |
708
+ | `live-transcript` | Fetch new sentences from an in-progress meeting (`--event-id` or `--media-id`, `--since-end-in-sec`) |
705
709
  | `create-text <name>` | Create a text note (`--text` or pipe via stdin) |
706
710
 
707
711
  #### CLI options
package/dist/index.js CHANGED
@@ -1335,7 +1335,7 @@ function register(server, client) {
1335
1335
  registerSpeakTool(
1336
1336
  server,
1337
1337
  "get_transcript",
1338
- "Retrieve the full transcript for a processed media file with speaker labels and timestamps. The media must be in 'processed' state. Use update_transcript_speakers to rename speaker labels after reviewing. For subtitle-formatted output, use get_captions instead.",
1338
+ "Retrieve the full transcript for a media file with speaker labels and timestamps. Works on processed media and also returns the partial, in-progress transcript while a meeting bot is still recording (LIVE_TRANSCRIPT state). To fetch only the new sentences added since your previous call during a live meeting, use get_live_meeting_transcript instead. Use update_transcript_speakers to rename speaker labels after reviewing. For subtitle-formatted output, use get_captions instead.",
1339
1339
  {
1340
1340
  mediaId: import_zod2.z.string().min(1).describe("Unique identifier of the media file")
1341
1341
  },
@@ -3185,7 +3185,7 @@ function register8(server, client) {
3185
3185
  "list_meeting_events",
3186
3186
  "List scheduled or completed meeting assistant events with filtering and pagination.",
3187
3187
  {
3188
- platformType: import_zod9.z.string().optional().describe("Filter by platform (e.g. zoom, teams, meet)"),
3188
+ platformType: import_zod9.z.string().optional().describe("Filter by platform. Allowed values: zoom, googleMeet, microsoftTeams, webex. Comma-separate for multiple. Must match these exact strings \u2014 server validates strictly."),
3189
3189
  meetingStatus: import_zod9.z.string().optional().describe("Filter by status (e.g. scheduled, completed, cancelled)"),
3190
3190
  page: import_zod9.z.number().int().min(0).optional().describe("Page number (0-based, default: 0)"),
3191
3191
  pageSize: import_zod9.z.number().int().min(1).max(500).optional().describe("Results per page (default: 20, max: 500)")
@@ -3309,6 +3309,90 @@ function register8(server, client) {
3309
3309
  }
3310
3310
  }
3311
3311
  );
3312
+ registerSpeakTool(
3313
+ server,
3314
+ "get_live_meeting_transcript",
3315
+ "Fetch new sentences from an in-progress or just-ended meeting transcript. Identify the meeting via meetingAssistantEventId (preferred) or mediaId. Pass back the previous response's nextCursor as sinceEndInSec to receive only what's been added since.",
3316
+ {
3317
+ meetingAssistantEventId: import_zod9.z.string().optional().describe("Meeting assistant event id from list_meeting_events. Either this or mediaId is required."),
3318
+ mediaId: import_zod9.z.string().optional().describe("Media id of the live meeting. Either this or meetingAssistantEventId is required."),
3319
+ sinceEndInSec: import_zod9.z.number().min(0).optional().describe("Pass the nextCursor value from your previous response to skip already-seen sentences. Omit on the first call.")
3320
+ },
3321
+ {
3322
+ title: "Get Live Meeting Transcript",
3323
+ readOnlyHint: true,
3324
+ destructiveHint: false,
3325
+ idempotentHint: false,
3326
+ openWorldHint: true
3327
+ },
3328
+ async ({ meetingAssistantEventId, mediaId, sinceEndInSec }) => {
3329
+ if (!meetingAssistantEventId && !mediaId) {
3330
+ return {
3331
+ content: [{ type: "text", text: "Error: provide either meetingAssistantEventId or mediaId." }],
3332
+ isError: true
3333
+ };
3334
+ }
3335
+ try {
3336
+ let resolvedMediaId = mediaId;
3337
+ let meetingStatus = null;
3338
+ let meetingName;
3339
+ if (meetingAssistantEventId) {
3340
+ const eventsRes = await api.get("/v1/meeting-assistant/events", {
3341
+ params: { pageSize: 50, sortBy: "startTime:desc" }
3342
+ });
3343
+ const events = eventsRes.data?.data?.events ?? eventsRes.data?.events ?? [];
3344
+ const event = events.find((e) => e.meetingAssistantEventId === meetingAssistantEventId);
3345
+ if (!event) {
3346
+ return {
3347
+ content: [{ type: "text", text: JSON.stringify({ status: "not_found", meetingAssistantEventId }, null, 2) }],
3348
+ structuredContent: { data: { status: "not_found", meetingAssistantEventId } }
3349
+ };
3350
+ }
3351
+ meetingStatus = event.currentStatus ?? null;
3352
+ meetingName = event.title;
3353
+ const mediaRef = event.mediaId;
3354
+ const linkedMediaId = typeof mediaRef === "string" ? mediaRef : mediaRef?.mediaId;
3355
+ if (!linkedMediaId) {
3356
+ const payload2 = {
3357
+ status: "not_started",
3358
+ meetingAssistantEventId,
3359
+ meetingStatus,
3360
+ message: "Meeting has no linked media yet \u2014 the bot may not have joined or started recording."
3361
+ };
3362
+ return {
3363
+ content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }],
3364
+ structuredContent: { data: payload2 }
3365
+ };
3366
+ }
3367
+ resolvedMediaId = linkedMediaId;
3368
+ }
3369
+ const transcriptRes = await api.get(`/v1/media/transcript/${resolvedMediaId}`, {
3370
+ params: Number.isFinite(sinceEndInSec) ? { sinceEndInSec } : void 0
3371
+ });
3372
+ const data = transcriptRes.data?.data ?? transcriptRes.data ?? {};
3373
+ const sentences = data?.insight?.transcript ?? [];
3374
+ const maxEnd = sentences.reduce((m, s) => Math.max(m, s.instances?.[0]?.endInSec ?? 0), 0);
3375
+ const nextCursor = sentences.length > 0 ? maxEnd : sinceEndInSec ?? 0;
3376
+ const payload = {
3377
+ mediaId: resolvedMediaId,
3378
+ name: data?.name ?? meetingName ?? null,
3379
+ meetingStatus,
3380
+ isLive: meetingStatus === "inCallRecording",
3381
+ newSentences: sentences,
3382
+ nextCursor
3383
+ };
3384
+ return {
3385
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
3386
+ structuredContent: { data: payload }
3387
+ };
3388
+ } catch (err) {
3389
+ return {
3390
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
3391
+ isError: true
3392
+ };
3393
+ }
3394
+ }
3395
+ );
3312
3396
  }
3313
3397
  var import_zod9;
3314
3398
  var init_meeting3 = __esm({
@@ -5380,6 +5464,38 @@ function createCli() {
5380
5464
  process.exit(1);
5381
5465
  }
5382
5466
  });
5467
+ program.command("list-meeting-events").description("List scheduled or completed meeting assistant events").option("-P, --platform <type>", "Filter by platform: zoom, googleMeet, microsoftTeams, webex (comma-separate for multiple)").option("-S, --status <status>", "Filter by meeting status (comma-separate for multiple)").option("-p, --page <n>", "Page number (0-based)", "0").option("-s, --page-size <n>", "Results per page", "20").option("--sort <field>", "Sort field", "startTime:desc").option("--json", "Output raw JSON").action(async (opts) => {
5468
+ requireApiKey();
5469
+ const client = await getClient();
5470
+ try {
5471
+ const params = {
5472
+ page: parseInt(opts.page),
5473
+ pageSize: parseInt(opts.pageSize),
5474
+ sortBy: opts.sort
5475
+ };
5476
+ if (opts.platform) params.platformType = opts.platform;
5477
+ if (opts.status) params.meetingStatus = opts.status;
5478
+ const res = await client.get("/v1/meeting-assistant/events", { params });
5479
+ const data = res.data?.data;
5480
+ if (opts.json) {
5481
+ printJson(data);
5482
+ return;
5483
+ }
5484
+ const events = data?.events ?? [];
5485
+ console.log(`Total: ${data?.totalCount ?? events.length}
5486
+ `);
5487
+ printTable(events, [
5488
+ { key: "meetingAssistantEventId", label: "Event ID", width: 24 },
5489
+ { key: "title", label: "Title", width: 32 },
5490
+ { key: "platform", label: "Platform", width: 16 },
5491
+ { key: "currentStatus", label: "Status", width: 18 },
5492
+ { key: "startTime", label: "Start", width: 20 }
5493
+ ]);
5494
+ } catch (err) {
5495
+ printError(err.response?.data?.message ?? err.message);
5496
+ process.exit(1);
5497
+ }
5498
+ });
5383
5499
  program.command("schedule-meeting").description("Schedule AI assistant to join a meeting").argument("<url>", "Meeting URL (Zoom, Meet, Teams)").option("-t, --title <title>", "Meeting title").option("-d, --date <datetime>", "Meeting date/time (ISO 8601, omit to join now)").option("-l, --language <lang>", "Meeting language", "en-US").option("--json", "Output raw JSON").action(async (url, opts) => {
5384
5500
  requireApiKey();
5385
5501
  const client = await getClient();
@@ -5406,6 +5522,66 @@ function createCli() {
5406
5522
  process.exit(1);
5407
5523
  }
5408
5524
  });
5525
+ program.command("live-transcript").description("Fetch new sentences from an in-progress or just-ended meeting").option("-e, --event-id <id>", "Meeting assistant event id (use `speakai-mcp list-meeting-events` to find it)").option("-m, --media-id <id>", "Media id (alternative to --event-id)").option("-s, --since-end-in-sec <seconds>", "nextCursor from previous call; omit on first call", parseFloat).option("--json", "Output raw JSON").action(async (opts) => {
5526
+ requireApiKey();
5527
+ const client = await getClient();
5528
+ if (!opts.eventId && !opts.mediaId) {
5529
+ printError("Provide --event-id or --media-id");
5530
+ process.exit(1);
5531
+ }
5532
+ try {
5533
+ let resolvedMediaId = opts.mediaId;
5534
+ let meetingStatus = null;
5535
+ let meetingName;
5536
+ if (opts.eventId) {
5537
+ const eventsRes = await client.get("/v1/meeting-assistant/events", {
5538
+ params: { pageSize: 50, sortBy: "startTime:desc" }
5539
+ });
5540
+ const events = eventsRes.data?.data?.events ?? eventsRes.data?.events ?? [];
5541
+ const event = events.find((e) => e.meetingAssistantEventId === opts.eventId);
5542
+ if (!event) {
5543
+ printError(`Meeting event not found: ${opts.eventId}`);
5544
+ process.exit(1);
5545
+ }
5546
+ meetingStatus = event.currentStatus ?? null;
5547
+ meetingName = event.title;
5548
+ const mediaRef = event.mediaId;
5549
+ resolvedMediaId = typeof mediaRef === "string" ? mediaRef : mediaRef?.mediaId;
5550
+ if (!resolvedMediaId) {
5551
+ printError("Meeting has no linked media yet \u2014 bot has not joined or started recording.");
5552
+ process.exit(1);
5553
+ }
5554
+ }
5555
+ const transcriptRes = await client.get(`/v1/media/transcript/${resolvedMediaId}`, {
5556
+ params: Number.isFinite(opts.sinceEndInSec) ? { sinceEndInSec: opts.sinceEndInSec } : void 0
5557
+ });
5558
+ const data = transcriptRes.data?.data ?? transcriptRes.data ?? {};
5559
+ const sentences = data?.insight?.transcript ?? [];
5560
+ const maxEnd = sentences.reduce((m, s) => Math.max(m, s.instances?.[0]?.endInSec ?? 0), 0);
5561
+ const nextCursor = sentences.length > 0 ? maxEnd : opts.sinceEndInSec ?? 0;
5562
+ const payload = {
5563
+ mediaId: resolvedMediaId,
5564
+ name: data?.name ?? meetingName ?? null,
5565
+ meetingStatus,
5566
+ isLive: meetingStatus === "inCallRecording",
5567
+ newSentences: sentences,
5568
+ nextCursor
5569
+ };
5570
+ if (opts.json) {
5571
+ printJson(payload);
5572
+ } else {
5573
+ console.log(`Meeting: ${payload.name ?? resolvedMediaId}`);
5574
+ console.log(`Status: ${payload.meetingStatus ?? "unknown"} (isLive=${payload.isLive})`);
5575
+ console.log(`New sentences: ${sentences.length} \u2022 nextCursor: ${nextCursor}`);
5576
+ for (const s of sentences) {
5577
+ console.log(` [${s.speakerId ?? "?"}] ${s.text ?? ""}`);
5578
+ }
5579
+ }
5580
+ } catch (err) {
5581
+ printError(err.response?.data?.message ?? err.message);
5582
+ process.exit(1);
5583
+ }
5584
+ });
5409
5585
  return program;
5410
5586
  }
5411
5587
  var import_commander, import_readline;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speakai/mcp-server",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "mcpName": "io.github.speakai/mcp-server",
5
5
  "description": "Official Speak AI MCP Server — capture meetings, search thousands of recordings, run async voice and video surveys, create clips, and automate workflows from your AI assistant.",
6
6
  "homepage": "https://mcp.speakai.co",