@speakai/mcp-server 1.12.3 → 1.13.1

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 +46 -7
  2. package/dist/index.js +1367 -348
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3666,8 +3666,16 @@ function register10(server, client) {
3666
3666
  registerSpeakTool(
3667
3667
  server,
3668
3668
  "list_automations",
3669
- "List all automation rules configured in the workspace.",
3670
- {},
3669
+ "List automation rules in the workspace, with paging and filters.",
3670
+ {
3671
+ page: import_zod11.z.number().int().min(0).optional().describe("0-based page index"),
3672
+ pageSize: import_zod11.z.number().int().min(1).max(100).optional().describe("Results per page"),
3673
+ sortBy: import_zod11.z.string().optional().describe('Sort expression, e.g. "createdAt:desc"'),
3674
+ query: import_zod11.z.string().optional().describe("Free-text search over automation names"),
3675
+ folderIds: import_zod11.z.string().optional().describe("Comma-separated folder ids to filter by"),
3676
+ isActive: import_zod11.z.boolean().optional().describe("Filter by active state"),
3677
+ runType: import_zod11.z.enum(["instant", "schedule"]).optional().describe("Filter by run type")
3678
+ },
3671
3679
  {
3672
3680
  title: "List Automations",
3673
3681
  readOnlyHint: true,
@@ -3675,9 +3683,35 @@ function register10(server, client) {
3675
3683
  idempotentHint: true,
3676
3684
  openWorldHint: false
3677
3685
  },
3686
+ async (params) => {
3687
+ try {
3688
+ const result = await api.get("/v1/automations", { params });
3689
+ return {
3690
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3691
+ };
3692
+ } catch (err) {
3693
+ return {
3694
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
3695
+ isError: true
3696
+ };
3697
+ }
3698
+ }
3699
+ );
3700
+ registerSpeakTool(
3701
+ server,
3702
+ "list_automation_names",
3703
+ "List automations as lightweight { name, id } pairs \u2014 useful for pickers without fetching full configs.",
3704
+ {},
3705
+ {
3706
+ title: "List Automation Names",
3707
+ readOnlyHint: true,
3708
+ destructiveHint: false,
3709
+ idempotentHint: true,
3710
+ openWorldHint: false
3711
+ },
3678
3712
  async () => {
3679
3713
  try {
3680
- const result = await api.get("/v1/automations");
3714
+ const result = await api.get("/v1/automations/list");
3681
3715
  return {
3682
3716
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3683
3717
  };
@@ -3692,7 +3726,7 @@ function register10(server, client) {
3692
3726
  registerSpeakTool(
3693
3727
  server,
3694
3728
  "get_automation",
3695
- "Get detailed information about a specific automation rule.",
3729
+ "Get detailed information about a specific automation rule, including its trigger and step graph.",
3696
3730
  {
3697
3731
  automationId: import_zod11.z.string().min(1).describe("Unique identifier of the automation")
3698
3732
  },
@@ -3719,21 +3753,40 @@ function register10(server, client) {
3719
3753
  );
3720
3754
  registerSpeakTool(
3721
3755
  server,
3722
- "create_automation",
3723
- "Create a new automation rule for automatic media processing workflows.",
3756
+ "get_automation_runs",
3757
+ "Get the run history (executions) for an automation, with paging and optional status filter.",
3724
3758
  {
3725
- name: import_zod11.z.string().min(1).describe("Display name for the automation"),
3726
- trigger: import_zod11.z.record(import_zod11.z.unknown()).describe(
3727
- 'Trigger object. Keys: `type` (e.g. "folders") and `folderIds` (array of folder IDs).'
3728
- ),
3729
- action: import_zod11.z.record(import_zod11.z.unknown()).describe(
3730
- 'Single action object (not an array). Keys: `type` ("magic-prompt" or "translation"). For magic-prompt, include a `magicPrompt` object ({ prompt, title?, assistantType?, fieldIds?, ... }). For translation, include a `translation` object ({ targetLanguage }).'
3731
- ),
3732
- description: import_zod11.z.string().optional().describe("Optional description"),
3733
- isActive: import_zod11.z.boolean().optional().describe("Whether the automation is active (defaults to true)"),
3734
- runType: import_zod11.z.string().optional().describe('Run type, e.g. "instant" (default) or "scheduled"'),
3735
- schedule: import_zod11.z.record(import_zod11.z.unknown()).optional().describe("Schedule object for scheduled automations: { timePeriod, repeatAt }")
3759
+ automationId: import_zod11.z.string().min(1).describe("Unique identifier of the automation"),
3760
+ page: import_zod11.z.number().int().min(0).optional().describe("0-based page index"),
3761
+ pageSize: import_zod11.z.number().int().min(1).max(100).optional().describe("Results per page"),
3762
+ status: import_zod11.z.enum(["pending", "running", "completed", "failed", "killed"]).optional().describe("Filter runs by status")
3763
+ },
3764
+ {
3765
+ title: "Get Automation Runs",
3766
+ readOnlyHint: true,
3767
+ destructiveHint: false,
3768
+ idempotentHint: true,
3769
+ openWorldHint: false
3736
3770
  },
3771
+ async ({ automationId, ...params }) => {
3772
+ try {
3773
+ const result = await api.get(`/v1/automations/${automationId}/runs`, { params });
3774
+ return {
3775
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3776
+ };
3777
+ } catch (err) {
3778
+ return {
3779
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
3780
+ isError: true
3781
+ };
3782
+ }
3783
+ }
3784
+ );
3785
+ registerSpeakTool(
3786
+ server,
3787
+ "create_automation",
3788
+ "Create a new automation rule using the V2 graph model (trigger + ordered steps). Fetch valid step/trigger options with list_automation_triggers / list_automation_actions if unsure.",
3789
+ writeSchema,
3737
3790
  {
3738
3791
  title: "Create Automation",
3739
3792
  readOnlyHint: false,
@@ -3758,20 +3811,10 @@ function register10(server, client) {
3758
3811
  registerSpeakTool(
3759
3812
  server,
3760
3813
  "update_automation",
3761
- "Update an existing automation rule. This replaces the whole automation, so fetch the current values with get_automation first and pass them all back.",
3814
+ "Update an existing automation rule. This replaces the whole automation (name, trigger, and steps), so fetch the current values with get_automation first and pass them all back with your changes.",
3762
3815
  {
3763
3816
  automationId: import_zod11.z.string().min(1).describe("Unique identifier of the automation"),
3764
- name: import_zod11.z.string().min(1).describe("Display name for the automation"),
3765
- trigger: import_zod11.z.record(import_zod11.z.unknown()).describe(
3766
- 'Trigger object. Keys: `type` (e.g. "folders") and `folderIds` (array of folder IDs).'
3767
- ),
3768
- action: import_zod11.z.record(import_zod11.z.unknown()).describe(
3769
- 'Single action object (not an array). Keys: `type` ("magic-prompt" or "translation"). For magic-prompt, include a `magicPrompt` object ({ prompt, title?, assistantType?, fieldIds?, ... }). For translation, include a `translation` object ({ targetLanguage }).'
3770
- ),
3771
- description: import_zod11.z.string().optional().describe("Optional description"),
3772
- isActive: import_zod11.z.boolean().optional().describe("Whether the automation is active (defaults to true)"),
3773
- runType: import_zod11.z.string().optional().describe('Run type, e.g. "instant" (default) or "scheduled"'),
3774
- schedule: import_zod11.z.record(import_zod11.z.unknown()).optional().describe("Schedule object for scheduled automations: { timePeriod, repeatAt }")
3817
+ ...writeSchema
3775
3818
  },
3776
3819
  {
3777
3820
  title: "Update Automation",
@@ -3782,10 +3825,7 @@ function register10(server, client) {
3782
3825
  },
3783
3826
  async ({ automationId, ...body }) => {
3784
3827
  try {
3785
- const result = await api.put(
3786
- `/v1/automations/${automationId}`,
3787
- body
3788
- );
3828
+ const result = await api.put(`/v1/automations/${automationId}`, body);
3789
3829
  return {
3790
3830
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3791
3831
  };
@@ -3813,9 +3853,204 @@ function register10(server, client) {
3813
3853
  },
3814
3854
  async ({ automationId }) => {
3815
3855
  try {
3816
- const result = await api.put(
3817
- `/v1/automations/status/${automationId}`
3818
- );
3856
+ const result = await api.put(`/v1/automations/status/${automationId}`);
3857
+ return {
3858
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3859
+ };
3860
+ } catch (err) {
3861
+ return {
3862
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
3863
+ isError: true
3864
+ };
3865
+ }
3866
+ }
3867
+ );
3868
+ registerSpeakTool(
3869
+ server,
3870
+ "bulk_update_automation_status",
3871
+ "Activate or deactivate multiple automations at once.",
3872
+ {
3873
+ automationIds: import_zod11.z.array(import_zod11.z.string().min(1)).min(1).max(100).describe("Automation ids to update"),
3874
+ isActive: import_zod11.z.boolean().describe("true to activate, false to deactivate, for all listed automations")
3875
+ },
3876
+ {
3877
+ title: "Bulk Update Automation Status",
3878
+ readOnlyHint: false,
3879
+ destructiveHint: false,
3880
+ idempotentHint: true,
3881
+ openWorldHint: false
3882
+ },
3883
+ async (body) => {
3884
+ try {
3885
+ const result = await api.put("/v1/automations/bulk/status", body);
3886
+ return {
3887
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3888
+ };
3889
+ } catch (err) {
3890
+ return {
3891
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
3892
+ isError: true
3893
+ };
3894
+ }
3895
+ }
3896
+ );
3897
+ registerSpeakTool(
3898
+ server,
3899
+ "bulk_assign_automation_folders",
3900
+ "Set the folder scope for multiple automations at once. Pass an empty folderIds array to remove the folder restriction (run on all folders).",
3901
+ {
3902
+ automationIds: import_zod11.z.array(import_zod11.z.string().min(1)).min(1).max(100).describe("Automation ids to update"),
3903
+ folderIds: import_zod11.z.array(import_zod11.z.string().min(1)).max(50).describe("Folder ids to scope the automations to. Empty array = all folders.")
3904
+ },
3905
+ {
3906
+ title: "Bulk Assign Automation Folders",
3907
+ readOnlyHint: false,
3908
+ destructiveHint: false,
3909
+ idempotentHint: true,
3910
+ openWorldHint: false
3911
+ },
3912
+ async (body) => {
3913
+ try {
3914
+ const result = await api.put("/v1/automations/bulk/folders", body);
3915
+ return {
3916
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3917
+ };
3918
+ } catch (err) {
3919
+ return {
3920
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
3921
+ isError: true
3922
+ };
3923
+ }
3924
+ }
3925
+ );
3926
+ registerSpeakTool(
3927
+ server,
3928
+ "run_automations",
3929
+ "Manually run one or more automations against one or more media items now (outside the normal trigger).",
3930
+ {
3931
+ mediaIds: import_zod11.z.array(import_zod11.z.string().min(1)).min(1).describe("Media ids to run the automations against"),
3932
+ automationIds: import_zod11.z.array(import_zod11.z.string().min(1)).min(1).describe("Automation ids to run")
3933
+ },
3934
+ {
3935
+ title: "Run Automations",
3936
+ readOnlyHint: false,
3937
+ destructiveHint: false,
3938
+ idempotentHint: false,
3939
+ openWorldHint: false
3940
+ },
3941
+ async (body) => {
3942
+ try {
3943
+ const result = await api.post("/v1/automations/run", body);
3944
+ return {
3945
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3946
+ };
3947
+ } catch (err) {
3948
+ return {
3949
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
3950
+ isError: true
3951
+ };
3952
+ }
3953
+ }
3954
+ );
3955
+ registerSpeakTool(
3956
+ server,
3957
+ "delete_automation",
3958
+ "Permanently delete an automation rule.",
3959
+ {
3960
+ automationId: import_zod11.z.string().min(1).describe("Unique identifier of the automation to delete")
3961
+ },
3962
+ {
3963
+ title: "Delete Automation",
3964
+ readOnlyHint: false,
3965
+ destructiveHint: true,
3966
+ idempotentHint: true,
3967
+ openWorldHint: false
3968
+ },
3969
+ async ({ automationId }) => {
3970
+ try {
3971
+ const result = await api.delete(`/v1/automations/${automationId}`);
3972
+ return {
3973
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3974
+ };
3975
+ } catch (err) {
3976
+ return {
3977
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
3978
+ isError: true
3979
+ };
3980
+ }
3981
+ }
3982
+ );
3983
+ registerSpeakTool(
3984
+ server,
3985
+ "list_automation_apps",
3986
+ "List the apps available in the automation catalog (e.g. Speak native + connected integrations). Use to discover what triggers/actions exist before building an automation.",
3987
+ {},
3988
+ {
3989
+ title: "List Automation Apps",
3990
+ readOnlyHint: true,
3991
+ destructiveHint: false,
3992
+ idempotentHint: true,
3993
+ openWorldHint: false
3994
+ },
3995
+ async () => {
3996
+ try {
3997
+ const result = await api.get("/v1/automations/catalog/apps");
3998
+ return {
3999
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4000
+ };
4001
+ } catch (err) {
4002
+ return {
4003
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4004
+ isError: true
4005
+ };
4006
+ }
4007
+ }
4008
+ );
4009
+ registerSpeakTool(
4010
+ server,
4011
+ "list_automation_triggers",
4012
+ "List the trigger types available in the automation catalog. Optionally filter by app.",
4013
+ {
4014
+ app: import_zod11.z.string().min(1).max(100).optional().describe("Filter triggers to a specific app slug")
4015
+ },
4016
+ {
4017
+ title: "List Automation Triggers",
4018
+ readOnlyHint: true,
4019
+ destructiveHint: false,
4020
+ idempotentHint: true,
4021
+ openWorldHint: false
4022
+ },
4023
+ async (params) => {
4024
+ try {
4025
+ const result = await api.get("/v1/automations/catalog/triggers", { params });
4026
+ return {
4027
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4028
+ };
4029
+ } catch (err) {
4030
+ return {
4031
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4032
+ isError: true
4033
+ };
4034
+ }
4035
+ }
4036
+ );
4037
+ registerSpeakTool(
4038
+ server,
4039
+ "list_automation_actions",
4040
+ "List the action/step types available in the automation catalog. Optionally filter by app.",
4041
+ {
4042
+ app: import_zod11.z.string().min(1).max(100).optional().describe("Filter actions to a specific app slug")
4043
+ },
4044
+ {
4045
+ title: "List Automation Actions",
4046
+ readOnlyHint: true,
4047
+ destructiveHint: false,
4048
+ idempotentHint: true,
4049
+ openWorldHint: false
4050
+ },
4051
+ async (params) => {
4052
+ try {
4053
+ const result = await api.get("/v1/automations/catalog/actions", { params });
3819
4054
  return {
3820
4055
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
3821
4056
  };
@@ -3828,13 +4063,26 @@ function register10(server, client) {
3828
4063
  }
3829
4064
  );
3830
4065
  }
3831
- var import_zod11;
4066
+ var import_zod11, STEPS_DESCRIPTION, TRIGGER_DESCRIPTION, writeSchema;
3832
4067
  var init_automations = __esm({
3833
4068
  "src/tools/automations.ts"() {
3834
4069
  "use strict";
3835
4070
  import_zod11 = require("zod");
3836
4071
  init_helpers();
3837
4072
  init_client();
4073
+ STEPS_DESCRIPTION = 'Ordered array of graph steps (1-20). Each step is an object: { stepId: string (unique within the array), stepType: one of "magic-prompt" | "translation" | "filter" | "composio-action" | "speak-upload", dependsOn?: string[] (stepIds this step runs after) } plus ONE payload key matching stepType:\n- magic-prompt -> magicPrompt: { prompt (required unless fieldIds given), title?, assistantType? ("general"|"researcher"|"marketer"|"sales"|"recruiter"|"custom", default "general"), assistantTemplateId? (required if assistantType="custom"), fieldIds?: string[] (max 10, for field extraction) }\n- translation -> translation: { targetLanguage: BCP-47 code, e.g. "es" }\n- filter -> filter: { logic: "AND"|"OR", rules: [{ field, op: "eq"|"neq"|"contains"|"ncontains"|"startsWith"|"gt"|"lt"|"exists", value? }] }\n- composio-action -> composio: { app, action, connectedAccountId?, argsTemplate? } (Composio is currently behind a server flag and may be unavailable)\n- speak-upload -> speakUpload: { folderId, sourceMode: "url"|"file", sourceUrl? (required when sourceMode="url"), name?, fieldsMap?, language? }';
4074
+ TRIGGER_DESCRIPTION = 'Trigger object. For folder-based automations: { type: "folders", folderIds: string[] } (at least one folder is required). Note: only "folders" is currently supported for multi-step automations via the API \u2014 "tags"/"keywords" are rejected, and "composio"/"webhook" triggers (provider, app, triggerSlug, webhookId, childKey, connectedAccountId) are gated by server flags.';
4075
+ writeSchema = {
4076
+ name: import_zod11.z.string().min(1).max(150).describe("Display name for the automation"),
4077
+ trigger: import_zod11.z.record(import_zod11.z.unknown()).describe(TRIGGER_DESCRIPTION),
4078
+ steps: import_zod11.z.array(import_zod11.z.record(import_zod11.z.unknown())).min(1).max(20).describe(STEPS_DESCRIPTION),
4079
+ description: import_zod11.z.string().max(1e3).optional().describe("Optional description"),
4080
+ isActive: import_zod11.z.boolean().optional().describe("Whether the automation is active (defaults to true)"),
4081
+ runType: import_zod11.z.enum(["instant", "schedule"]).optional().describe('Run type: "instant" (default, runs on trigger) or "schedule" (cron)'),
4082
+ schedule: import_zod11.z.record(import_zod11.z.unknown()).optional().describe(
4083
+ 'Required when runType="schedule": { timePeriod: "today"|"yesterday"|"last7days"|"last14days"|"thisWeek", repeatAt: string }'
4084
+ )
4085
+ };
3838
4086
  }
3839
4087
  });
3840
4088
 
@@ -4109,10 +4357,799 @@ function register13(server, client) {
4109
4357
  idempotentHint: true,
4110
4358
  openWorldHint: false
4111
4359
  },
4112
- async ({ clipId, ...params }) => {
4360
+ async ({ clipId, ...params }) => {
4361
+ try {
4362
+ const url = clipId ? `/v1/clips/${clipId}` : "/v1/clips";
4363
+ const result = await api.get(url, { params });
4364
+ return {
4365
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4366
+ };
4367
+ } catch (err) {
4368
+ return {
4369
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4370
+ isError: true
4371
+ };
4372
+ }
4373
+ }
4374
+ );
4375
+ registerSpeakTool(
4376
+ server,
4377
+ "update_clip",
4378
+ "Update a clip's title, description, or tags.",
4379
+ {
4380
+ clipId: import_zod14.z.string().min(1).describe("ID of the clip to update"),
4381
+ title: import_zod14.z.string().optional().describe("New title"),
4382
+ description: import_zod14.z.string().optional().describe("New description"),
4383
+ tags: import_zod14.z.array(import_zod14.z.string()).optional().describe("New tags")
4384
+ },
4385
+ {
4386
+ title: "Update Clip",
4387
+ readOnlyHint: false,
4388
+ destructiveHint: false,
4389
+ idempotentHint: true,
4390
+ openWorldHint: false
4391
+ },
4392
+ async ({ clipId, ...body }) => {
4393
+ try {
4394
+ const result = await api.put(`/v1/clips/${clipId}`, body);
4395
+ return {
4396
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4397
+ };
4398
+ } catch (err) {
4399
+ return {
4400
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4401
+ isError: true
4402
+ };
4403
+ }
4404
+ }
4405
+ );
4406
+ registerSpeakTool(
4407
+ server,
4408
+ "delete_clip",
4409
+ "Permanently delete a clip and its associated media file.",
4410
+ {
4411
+ clipId: import_zod14.z.string().min(1).describe("ID of the clip to delete")
4412
+ },
4413
+ {
4414
+ title: "Delete Clip",
4415
+ readOnlyHint: false,
4416
+ destructiveHint: true,
4417
+ idempotentHint: true,
4418
+ openWorldHint: false
4419
+ },
4420
+ async ({ clipId }) => {
4421
+ try {
4422
+ const result = await api.delete(`/v1/clips/${clipId}`);
4423
+ return {
4424
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4425
+ };
4426
+ } catch (err) {
4427
+ return {
4428
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4429
+ isError: true
4430
+ };
4431
+ }
4432
+ }
4433
+ );
4434
+ }
4435
+ var import_zod14;
4436
+ var init_clips = __esm({
4437
+ "src/tools/clips.ts"() {
4438
+ "use strict";
4439
+ import_zod14 = require("zod");
4440
+ init_helpers();
4441
+ init_client();
4442
+ init_dist();
4443
+ }
4444
+ });
4445
+
4446
+ // src/media-utils.ts
4447
+ function isVideoFile(filePath) {
4448
+ return VIDEO_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
4449
+ }
4450
+ function getMimeType(filePath) {
4451
+ const ext = path.extname(filePath).toLowerCase();
4452
+ const isVideo = isVideoFile(filePath);
4453
+ if (ext === ".mp4") return isVideo ? "video/mp4" : "audio/mp4";
4454
+ if (ext === ".webm") return isVideo ? "video/webm" : "audio/webm";
4455
+ return MIME_TYPES[ext] ?? (isVideo ? "video/mp4" : "audio/mpeg");
4456
+ }
4457
+ function detectMediaType(filePath) {
4458
+ return isVideoFile(filePath) ? "video" : "audio";
4459
+ }
4460
+ var path, VIDEO_EXTENSIONS, MIME_TYPES;
4461
+ var init_media_utils = __esm({
4462
+ "src/media-utils.ts"() {
4463
+ "use strict";
4464
+ path = __toESM(require("path"));
4465
+ VIDEO_EXTENSIONS = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".wmv"];
4466
+ MIME_TYPES = {
4467
+ ".mp3": "audio/mpeg",
4468
+ ".m4a": "audio/mp4",
4469
+ ".wav": "audio/wav",
4470
+ ".ogg": "audio/ogg",
4471
+ ".flac": "audio/flac",
4472
+ ".mov": "video/quicktime",
4473
+ ".avi": "video/x-msvideo",
4474
+ ".mkv": "video/x-matroska",
4475
+ ".wmv": "video/x-ms-wmv"
4476
+ };
4477
+ }
4478
+ });
4479
+
4480
+ // src/tools/workflows.ts
4481
+ var workflows_exports = {};
4482
+ __export(workflows_exports, {
4483
+ register: () => register14
4484
+ });
4485
+ function register14(server, client) {
4486
+ const api = client ?? speakClient;
4487
+ registerSpeakTool(
4488
+ server,
4489
+ "upload_and_analyze",
4490
+ "Upload and transcribe media from a URL \u2014 a direct/public file URL, OR a shareable social/video link (YouTube, Instagram, TikTok, X, Facebook, Reddit, SoundCloud, and similar), which Speak resolves to the underlying media automatically. Returns media_id immediately; after this returns, poll get_media_status until state is 'processed' (typically 1-3 min for under 60min audio), then call get_media_insights for AI summaries. This async pattern is required for remote MCP transports \u2014 long blocking calls die at proxy idle timeouts. (Vimeo links are not yet supported.)",
4491
+ {
4492
+ url: import_zod15.z.string().describe("Direct/public media file URL, or a shareable social/video page link (e.g. an Instagram reel, TikTok, YouTube, or X post URL) \u2014 page links are resolved to the underlying media server-side. Pass the URL the user gave you as-is."),
4493
+ name: import_zod15.z.string().optional().describe("Display name for the media (defaults to filename from URL)"),
4494
+ mediaType: import_zod15.z.enum([MediaType.AUDIO, MediaType.VIDEO]).optional().describe("Media type (default: audio)"),
4495
+ sourceLanguage: import_zod15.z.string().optional().describe("BCP-47 language code (e.g., 'en-US', 'he-IL')"),
4496
+ folderId: import_zod15.z.string().optional().describe("Folder ID to place the media in"),
4497
+ tags: import_zod15.z.string().optional().describe("Comma-separated tags")
4498
+ },
4499
+ {
4500
+ title: "Upload and Analyze Media",
4501
+ readOnlyHint: false,
4502
+ destructiveHint: false,
4503
+ idempotentHint: false,
4504
+ openWorldHint: false
4505
+ },
4506
+ async (params) => {
4507
+ try {
4508
+ const uploadBody = {
4509
+ name: params.name ?? params.url.split("/").pop()?.split("?")[0] ?? "Upload",
4510
+ url: params.url,
4511
+ mediaType: params.mediaType ?? "audio"
4512
+ };
4513
+ if (params.sourceLanguage) uploadBody.sourceLanguage = params.sourceLanguage;
4514
+ if (params.folderId) uploadBody.folderId = params.folderId;
4515
+ if (params.tags) uploadBody.tags = params.tags;
4516
+ const uploadRes = await api.post("/v1/media/upload", uploadBody);
4517
+ const mediaId = uploadRes.data?.data?.mediaId;
4518
+ const state = uploadRes.data?.data?.state ?? "pending";
4519
+ if (!mediaId) {
4520
+ return {
4521
+ content: [{ type: "text", text: `Error: Upload succeeded but no mediaId returned.
4522
+ ${JSON.stringify(uploadRes.data, null, 2)}` }],
4523
+ isError: true
4524
+ };
4525
+ }
4526
+ const result = {
4527
+ mediaId,
4528
+ state,
4529
+ message: "Upload accepted. Processing has started in the background.",
4530
+ nextSteps: [
4531
+ `1. Poll get_media_status with mediaId="${mediaId}" every 10-30 seconds.`,
4532
+ `2. When state is "processed" (typically 1-3 min for audio under 60 min), call get_media_insights for the AI summary and get_transcript for the full transcript.`,
4533
+ `3. If state becomes "failed", processing did not complete \u2014 surface the error to the user.`
4534
+ ]
4535
+ };
4536
+ return {
4537
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4538
+ };
4539
+ } catch (err) {
4540
+ return {
4541
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4542
+ isError: true
4543
+ };
4544
+ }
4545
+ }
4546
+ );
4547
+ registerSpeakTool(
4548
+ server,
4549
+ "upload_local_file",
4550
+ [
4551
+ "Upload a local file to Speak AI for transcription and analysis.",
4552
+ "Reads the file from disk, gets a pre-signed S3 URL, uploads the file, then creates the media entry.",
4553
+ "Works with any audio or video file on the local filesystem.",
4554
+ "After upload, use get_media_status to poll for completion, then get_transcript and get_media_insights."
4555
+ ].join(" "),
4556
+ {
4557
+ filePath: import_zod15.z.string().describe("Absolute path to the local audio or video file"),
4558
+ name: import_zod15.z.string().optional().describe("Display name (defaults to filename)"),
4559
+ mediaType: import_zod15.z.enum([MediaType.AUDIO, MediaType.VIDEO]).optional().describe("Media type (auto-detected from extension if omitted)"),
4560
+ sourceLanguage: import_zod15.z.string().optional().describe("BCP-47 language code (e.g., 'en-US')"),
4561
+ folderId: import_zod15.z.string().optional().describe("Folder ID to place the media in"),
4562
+ tags: import_zod15.z.string().optional().describe("Comma-separated tags")
4563
+ },
4564
+ {
4565
+ title: "Upload Local File",
4566
+ readOnlyHint: false,
4567
+ destructiveHint: false,
4568
+ idempotentHint: false,
4569
+ openWorldHint: false
4570
+ },
4571
+ async (params) => {
4572
+ try {
4573
+ const filePath = params.filePath;
4574
+ if (!fs.existsSync(filePath)) {
4575
+ return {
4576
+ content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
4577
+ isError: true
4578
+ };
4579
+ }
4580
+ const filename = path2.basename(filePath);
4581
+ const isVideo = isVideoFile(filePath);
4582
+ const mediaType = params.mediaType ?? detectMediaType(filePath);
4583
+ const mimeType = getMimeType(filePath);
4584
+ const signedRes = await api.get("/v1/media/upload/signedurl", {
4585
+ params: { isVideo, filename, mimeType }
4586
+ });
4587
+ const signedData = signedRes.data?.data;
4588
+ const uploadUrl = signedData?.preSignedUrl ?? signedData?.signedUrl ?? signedData?.url;
4589
+ if (!uploadUrl) {
4590
+ return {
4591
+ content: [{ type: "text", text: `Error: Could not get signed upload URL.
4592
+ ${JSON.stringify(signedRes.data, null, 2)}` }],
4593
+ isError: true
4594
+ };
4595
+ }
4596
+ const fileBuffer = fs.readFileSync(filePath);
4597
+ const axios2 = (await import("axios")).default;
4598
+ await axios2.put(uploadUrl, fileBuffer, {
4599
+ headers: {
4600
+ "Content-Type": mimeType
4601
+ },
4602
+ maxBodyLength: Infinity,
4603
+ maxContentLength: Infinity
4604
+ });
4605
+ const createBody = {
4606
+ name: params.name ?? filename,
4607
+ url: uploadUrl.split("?")[0],
4608
+ // S3 URL without query params; server re-signs via CloudFront
4609
+ mediaType
4610
+ };
4611
+ if (params.sourceLanguage) createBody.sourceLanguage = params.sourceLanguage;
4612
+ if (params.folderId) createBody.folderId = params.folderId;
4613
+ if (params.tags) createBody.tags = params.tags;
4614
+ const createRes = await api.post("/v1/media/upload", createBody);
4615
+ const data = createRes.data?.data;
4616
+ return {
4617
+ content: [
4618
+ {
4619
+ type: "text",
4620
+ text: JSON.stringify(
4621
+ {
4622
+ mediaId: data?.mediaId,
4623
+ state: data?.state,
4624
+ message: `File uploaded successfully. Use get_media_status to poll until state is 'processed', then use get_transcript and get_media_insights.`
4625
+ },
4626
+ null,
4627
+ 2
4628
+ )
4629
+ }
4630
+ ]
4631
+ };
4632
+ } catch (err) {
4633
+ return {
4634
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4635
+ isError: true
4636
+ };
4637
+ }
4638
+ }
4639
+ );
4640
+ }
4641
+ var import_zod15, fs, path2;
4642
+ var init_workflows = __esm({
4643
+ "src/tools/workflows.ts"() {
4644
+ "use strict";
4645
+ import_zod15 = require("zod");
4646
+ init_helpers();
4647
+ init_client();
4648
+ init_dist();
4649
+ fs = __toESM(require("fs"));
4650
+ path2 = __toESM(require("path"));
4651
+ init_media_utils();
4652
+ }
4653
+ });
4654
+
4655
+ // src/tools/users.ts
4656
+ var users_exports = {};
4657
+ __export(users_exports, {
4658
+ register: () => register15
4659
+ });
4660
+ function register15(server, client) {
4661
+ const api = client ?? speakClient;
4662
+ registerSpeakTool(
4663
+ server,
4664
+ "list_users",
4665
+ "List the users (members) in the workspace/company, with their ids, names, emails, and permissions. Use the returned _id values when assigning members to user groups.",
4666
+ {
4667
+ filterName: import_zod16.z.string().optional().describe(
4668
+ 'Search text. Plain text matches first/last name or email; prefix with "email:" or "name:" to scope, e.g. "email:jane@acme.com".'
4669
+ ),
4670
+ sortBy: import_zod16.z.string().optional().describe('Sort expression "field:asc" or "field:desc", e.g. "createdAt:desc", "email:asc"'),
4671
+ page: import_zod16.z.number().int().min(0).optional().describe("0-based page index (default 0)"),
4672
+ pageSize: import_zod16.z.number().int().min(1).max(200).optional().describe("Results per page (default 50)")
4673
+ },
4674
+ {
4675
+ title: "List Users",
4676
+ readOnlyHint: true,
4677
+ destructiveHint: false,
4678
+ idempotentHint: true,
4679
+ openWorldHint: false
4680
+ },
4681
+ async (params) => {
4682
+ try {
4683
+ const result = await api.get("/v1/admin/users", { params });
4684
+ return {
4685
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4686
+ };
4687
+ } catch (err) {
4688
+ return {
4689
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4690
+ isError: true
4691
+ };
4692
+ }
4693
+ }
4694
+ );
4695
+ registerSpeakTool(
4696
+ server,
4697
+ "list_user_groups",
4698
+ "List all user groups in the company. Each group includes its members (hydrated names/emails) and member ids. Use this to discover group ids and current membership before updating or deleting a group.",
4699
+ {},
4700
+ {
4701
+ title: "List User Groups",
4702
+ readOnlyHint: true,
4703
+ destructiveHint: false,
4704
+ idempotentHint: true,
4705
+ openWorldHint: false
4706
+ },
4707
+ async () => {
4708
+ try {
4709
+ const result = await api.get("/v1/admin/usergroup");
4710
+ return {
4711
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4712
+ };
4713
+ } catch (err) {
4714
+ return {
4715
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4716
+ isError: true
4717
+ };
4718
+ }
4719
+ }
4720
+ );
4721
+ registerSpeakTool(
4722
+ server,
4723
+ "create_user_group",
4724
+ "Create a new user group and assign members. Member ids come from list_users. Fails with a 409 if a group with the same name already exists in the company.",
4725
+ {
4726
+ description: import_zod16.z.string().min(1).describe("Group name"),
4727
+ users: import_zod16.z.array(import_zod16.z.string().min(1)).default([]).describe("User _id strings to add as members (fetch via list_users)")
4728
+ },
4729
+ {
4730
+ title: "Create User Group",
4731
+ readOnlyHint: false,
4732
+ destructiveHint: false,
4733
+ idempotentHint: false,
4734
+ openWorldHint: false
4735
+ },
4736
+ async (body) => {
4737
+ try {
4738
+ const result = await api.post("/v1/admin/usergroup", body);
4739
+ return {
4740
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4741
+ };
4742
+ } catch (err) {
4743
+ return {
4744
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4745
+ isError: true
4746
+ };
4747
+ }
4748
+ }
4749
+ );
4750
+ registerSpeakTool(
4751
+ server,
4752
+ "update_user_group",
4753
+ "Update a user group's name and member list. NOTE: the users array is a FULL REPLACEMENT, not a delta \u2014 any member id you omit is removed from the group. Fetch the current members with list_user_groups first and send the complete list.",
4754
+ {
4755
+ _id: import_zod16.z.string().min(1).describe("Group _id to update (from list_user_groups)"),
4756
+ description: import_zod16.z.string().min(1).describe("New group name"),
4757
+ users: import_zod16.z.array(import_zod16.z.string().min(1)).describe("Full replacement list of member _id strings (omitted users are removed)")
4758
+ },
4759
+ {
4760
+ title: "Update User Group",
4761
+ readOnlyHint: false,
4762
+ destructiveHint: false,
4763
+ idempotentHint: true,
4764
+ openWorldHint: false
4765
+ },
4766
+ async (body) => {
4767
+ try {
4768
+ const result = await api.put("/v1/admin/usergroup", body);
4769
+ return {
4770
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4771
+ };
4772
+ } catch (err) {
4773
+ return {
4774
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4775
+ isError: true
4776
+ };
4777
+ }
4778
+ }
4779
+ );
4780
+ registerSpeakTool(
4781
+ server,
4782
+ "delete_user_group",
4783
+ "Delete a user group. This removes the group only; it does not delete the users themselves.",
4784
+ {
4785
+ id: import_zod16.z.string().min(1).describe("Group _id to delete (from list_user_groups)")
4786
+ },
4787
+ {
4788
+ title: "Delete User Group",
4789
+ readOnlyHint: false,
4790
+ destructiveHint: true,
4791
+ idempotentHint: true,
4792
+ openWorldHint: false
4793
+ },
4794
+ async ({ id }) => {
4795
+ try {
4796
+ const result = await api.delete(`/v1/admin/usergroup/${id}`);
4797
+ return {
4798
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4799
+ };
4800
+ } catch (err) {
4801
+ return {
4802
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
4803
+ isError: true
4804
+ };
4805
+ }
4806
+ }
4807
+ );
4808
+ }
4809
+ var import_zod16;
4810
+ var init_users = __esm({
4811
+ "src/tools/users.ts"() {
4812
+ "use strict";
4813
+ import_zod16 = require("zod");
4814
+ init_helpers();
4815
+ init_client();
4816
+ }
4817
+ });
4818
+
4819
+ // src/tools/dashboard-widgets.ts
4820
+ function buildDashboardWidgets(items) {
4821
+ let y = 0;
4822
+ let rowX = 0;
4823
+ let rowH = 0;
4824
+ return items.map((item) => {
4825
+ const meta = WIDGET_META[item.type];
4826
+ const full = meta.w >= GRID_COLS;
4827
+ let x;
4828
+ if (full) {
4829
+ if (rowX !== 0) {
4830
+ y += rowH;
4831
+ rowX = 0;
4832
+ rowH = 0;
4833
+ }
4834
+ x = 0;
4835
+ const widget2 = makeWidget(item, x, y);
4836
+ y += meta.h;
4837
+ return widget2;
4838
+ }
4839
+ if (rowX + meta.w > GRID_COLS) {
4840
+ y += rowH;
4841
+ rowX = 0;
4842
+ rowH = 0;
4843
+ }
4844
+ x = rowX;
4845
+ const widget = makeWidget(item, x, y);
4846
+ rowX += meta.w;
4847
+ rowH = Math.max(rowH, meta.h);
4848
+ if (rowX >= GRID_COLS) {
4849
+ y += rowH;
4850
+ rowX = 0;
4851
+ rowH = 0;
4852
+ }
4853
+ return widget;
4854
+ });
4855
+ }
4856
+ function makeWidget(item, x, y) {
4857
+ const meta = WIDGET_META[item.type];
4858
+ return {
4859
+ id: (0, import_crypto.randomUUID)(),
4860
+ type: item.type,
4861
+ title: item.title ?? meta.titleDefault,
4862
+ config: item.config ?? {},
4863
+ layout: { x, y, w: meta.w, h: meta.h, minW: meta.minW, minH: meta.minH }
4864
+ };
4865
+ }
4866
+ var import_crypto, GRID_COLS, WIDGET_TYPES, WIDGET_META, WIDGET_CATALOG, COMMON_WIDGET_CONFIG, DASHBOARD_EXAMPLES;
4867
+ var init_dashboard_widgets = __esm({
4868
+ "src/tools/dashboard-widgets.ts"() {
4869
+ "use strict";
4870
+ import_crypto = require("crypto");
4871
+ GRID_COLS = 12;
4872
+ WIDGET_TYPES = [
4873
+ "stat-cards",
4874
+ "sentiment",
4875
+ "media-list",
4876
+ "field-distribution",
4877
+ "upload-timeline",
4878
+ "themes",
4879
+ "team-activity",
4880
+ "kpi-trend",
4881
+ "field-metric",
4882
+ "metric-group",
4883
+ "notes",
4884
+ "people",
4885
+ "comparison",
4886
+ "sentiment-trend"
4887
+ ];
4888
+ WIDGET_META = {
4889
+ "stat-cards": { w: GRID_COLS, h: 3, minW: 4, minH: 2, titleDefault: "Usage overview" },
4890
+ sentiment: { w: 6, h: 4, minW: 3, minH: 3, titleDefault: "Sentiment" },
4891
+ "media-list": { w: GRID_COLS, h: 4, minW: 4, minH: 3, titleDefault: "Recent media" },
4892
+ "field-distribution": { w: 6, h: 4, minW: 3, minH: 3, titleDefault: "Field breakdown" },
4893
+ "upload-timeline": { w: 6, h: 4, minW: 3, minH: 3, titleDefault: "Uploads over time" },
4894
+ themes: { w: 6, h: 4, minW: 3, minH: 3, titleDefault: "Themes" },
4895
+ "team-activity": { w: 6, h: 4, minW: 3, minH: 3, titleDefault: "Team activity" },
4896
+ "kpi-trend": { w: 3, h: 2, minW: 2, minH: 2, titleDefault: "Total files" },
4897
+ "field-metric": { w: 3, h: 2, minW: 2, minH: 2, titleDefault: "Field metric" },
4898
+ "metric-group": { w: 6, h: 2, minW: 3, minH: 2, titleDefault: "Metrics" },
4899
+ notes: { w: GRID_COLS, h: 2, minW: 2, minH: 1, titleDefault: "Note" },
4900
+ people: { w: 6, h: 4, minW: 3, minH: 3, titleDefault: "People" },
4901
+ comparison: { w: 6, h: 3, minW: 3, minH: 2, titleDefault: "Comparison" },
4902
+ "sentiment-trend": { w: 6, h: 4, minW: 3, minH: 3, titleDefault: "Sentiment over time" }
4903
+ };
4904
+ WIDGET_CATALOG = [
4905
+ {
4906
+ type: "stat-cards",
4907
+ purpose: "Headline totals for the scope (full width).",
4908
+ config: 'metrics?: string[] \u2014 any of "media", "storage", "words", "speakers", "duration", "sentiment"'
4909
+ },
4910
+ { type: "sentiment", purpose: "Sentiment distribution + representative sentences.", config: "none" },
4911
+ { type: "media-list", purpose: "Most recent media in scope (full width).", config: "none" },
4912
+ {
4913
+ type: "field-distribution",
4914
+ purpose: "Value-frequency breakdown for one custom field.",
4915
+ config: 'fieldId: string (a custom field id from list_fields); measure?: "count" | "words" | "duration" | "avg:<numericFieldId>" | "sum:<numericFieldId>"'
4916
+ },
4917
+ { type: "upload-timeline", purpose: "Uploads over time per media type.", config: "none" },
4918
+ { type: "themes", purpose: "Dominant theme clusters.", config: 'chartType?: "cloud" | "bar"' },
4919
+ { type: "team-activity", purpose: "Activity by team member.", config: "none" },
4920
+ {
4921
+ type: "kpi-trend",
4922
+ purpose: "A KPI with period-over-period delta.",
4923
+ config: 'metrics?: string[] \u2014 any of "totalFiles", "totalDurationSeconds", "totalWords", "uniqueSpeakers"'
4924
+ },
4925
+ {
4926
+ type: "field-metric",
4927
+ purpose: "A single custom field's aggregate, with a period-over-period delta.",
4928
+ config: 'fieldId: string (custom field id from list_fields); aggregator: "sum" | "avg" | "min" | "max" | "count"'
4929
+ },
4930
+ {
4931
+ type: "metric-group",
4932
+ purpose: "A scorecard of several field metrics in one card.",
4933
+ config: 'metrics: Array<{ fieldId: string, op: "sum"|"avg"|"min"|"max"|"count", label?: string }> (note the per-item key is `op`, not `aggregator`)'
4934
+ },
4935
+ {
4936
+ type: "notes",
4937
+ purpose: "Free-text note/context block (full width).",
4938
+ config: "heading?: string; content?: string"
4939
+ },
4940
+ { type: "people", purpose: "Speaker/people breakdown.", config: "none" },
4941
+ {
4942
+ type: "comparison",
4943
+ purpose: "Period-over-period comparison of KPIs.",
4944
+ config: 'metrics?: string[] \u2014 any of "totalFiles", "totalDurationSeconds", "totalWords", "uniqueSpeakers"'
4945
+ },
4946
+ { type: "sentiment-trend", purpose: "Sentiment over time.", config: "none" }
4947
+ ];
4948
+ COMMON_WIDGET_CONFIG = [
4949
+ {
4950
+ key: "accentColor",
4951
+ appliesTo: "any widget",
4952
+ description: 'Hex color for the widget accent, e.g. "#6366F1". Omit for the default.'
4953
+ },
4954
+ {
4955
+ key: "scopeOverride",
4956
+ appliesTo: "any widget except notes",
4957
+ description: "Override the dashboard's scope for just this widget: { datePreset: string, folderId: string }. Omit to inherit the dashboard scope."
4958
+ }
4959
+ ];
4960
+ DASHBOARD_EXAMPLES = [
4961
+ {
4962
+ name: "Sales calls overview",
4963
+ payload: {
4964
+ title: "Customer Calls Overview",
4965
+ folderScope: ["<folderId>"],
4966
+ dateRange: { preset: "last30days" },
4967
+ filters: { filterList: [{ fieldName: "Stage", fieldOperator: "is", fieldValue: ["Closed Won"] }] },
4968
+ widgets: [
4969
+ { type: "stat-cards", config: { metrics: ["media", "words", "speakers"] } },
4970
+ { type: "sentiment", config: { accentColor: "#6366F1" } },
4971
+ { type: "field-distribution", title: "Deals by stage", config: { fieldId: "<fieldId>", measure: "count" } },
4972
+ { type: "themes", config: { chartType: "bar" } },
4973
+ { type: "media-list" }
4974
+ ]
4975
+ }
4976
+ },
4977
+ {
4978
+ name: "Field-metrics scorecard (new widgets)",
4979
+ payload: {
4980
+ title: "Deal Metrics",
4981
+ folderScope: ["<folderId>"],
4982
+ dateRange: { preset: "thisQuarter" },
4983
+ widgets: [
4984
+ { type: "kpi-trend", title: "Total calls", config: { metrics: ["totalFiles"] } },
4985
+ { type: "field-metric", title: "Avg deal size", config: { fieldId: "<dealSizeFieldId>", aggregator: "avg" } },
4986
+ {
4987
+ type: "metric-group",
4988
+ title: "Pipeline",
4989
+ config: {
4990
+ metrics: [
4991
+ { fieldId: "<dealSizeFieldId>", op: "sum", label: "Total pipeline" },
4992
+ { fieldId: "<dealSizeFieldId>", op: "max", label: "Largest deal" }
4993
+ ]
4994
+ }
4995
+ },
4996
+ // Per-widget scope override: this card ignores the dashboard scope.
4997
+ { type: "field-metric", title: "EMEA avg", config: { fieldId: "<dealSizeFieldId>", aggregator: "avg", scopeOverride: { datePreset: "last7days", folderId: "<emeaFolderId>" } } }
4998
+ ]
4999
+ }
5000
+ }
5001
+ ];
5002
+ }
5003
+ });
5004
+
5005
+ // src/tools/dashboards.ts
5006
+ var dashboards_exports = {};
5007
+ __export(dashboards_exports, {
5008
+ register: () => register16
5009
+ });
5010
+ function withBuiltWidgets(body) {
5011
+ if (Array.isArray(body.widgets)) {
5012
+ return { ...body, widgets: buildDashboardWidgets(body.widgets) };
5013
+ }
5014
+ return body;
5015
+ }
5016
+ function register16(server, client) {
5017
+ const api = client ?? speakClient;
5018
+ registerSpeakTool(
5019
+ server,
5020
+ "list_dashboards",
5021
+ "List all analytics dashboards the caller can access, including share state.",
5022
+ {},
5023
+ {
5024
+ title: "List Dashboards",
5025
+ readOnlyHint: true,
5026
+ destructiveHint: false,
5027
+ idempotentHint: true,
5028
+ openWorldHint: false
5029
+ },
5030
+ async () => {
5031
+ try {
5032
+ const result = await api.get("/v1/dashboards");
5033
+ return {
5034
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
5035
+ };
5036
+ } catch (err) {
5037
+ return {
5038
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
5039
+ isError: true
5040
+ };
5041
+ }
5042
+ }
5043
+ );
5044
+ registerSpeakTool(
5045
+ server,
5046
+ "get_dashboard",
5047
+ "Get a single dashboard's full config (widgets, filters, date range, folder scope).",
5048
+ {
5049
+ dashboardId: import_zod17.z.string().min(1).describe("Dashboard business id (the dashboardId field from list_dashboards, not the Mongo _id)")
5050
+ },
5051
+ {
5052
+ title: "Get Dashboard",
5053
+ readOnlyHint: true,
5054
+ destructiveHint: false,
5055
+ idempotentHint: true,
5056
+ openWorldHint: false
5057
+ },
5058
+ async ({ dashboardId }) => {
5059
+ try {
5060
+ const result = await api.get(`/v1/dashboards/${dashboardId}`);
5061
+ return {
5062
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
5063
+ };
5064
+ } catch (err) {
5065
+ return {
5066
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
5067
+ isError: true
5068
+ };
5069
+ }
5070
+ }
5071
+ );
5072
+ registerSpeakTool(
5073
+ server,
5074
+ "list_dashboard_widgets",
5075
+ "Discovery + how-to helper for building and customizing dashboards. Returns every widget type with what it shows and the exact `config` keys it accepts (metrics, fieldId, aggregator, chartType, accentColor, scopeOverride, \u2026), the config that applies to all widgets, two complete worked example payloads (including the field-metric and metric-group cards), and tips for managing dashboards. Call this before create_dashboard / update_dashboard.",
5076
+ {},
5077
+ {
5078
+ title: "List Dashboard Widgets",
5079
+ readOnlyHint: true,
5080
+ destructiveHint: false,
5081
+ idempotentHint: true,
5082
+ openWorldHint: false
5083
+ },
5084
+ async () => {
5085
+ const data = {
5086
+ widgets: WIDGET_CATALOG,
5087
+ commonConfig: COMMON_WIDGET_CONFIG,
5088
+ notes: [
5089
+ "Pass widgets to create_dashboard as a simple ordered list; ids and grid layout are computed for you.",
5090
+ "Field-driven widgets (field-distribution, field-metric, metric-group) need custom field ids from list_fields in their config.",
5091
+ "field-metric uses config.aggregator; metric-group items use the key `op` (both: sum|avg|min|max|count).",
5092
+ "accentColor (hex) and scopeOverride ({datePreset, folderId}) can be set on any widget's config to customize it.",
5093
+ "Most other widgets render on sensible defaults with no config."
5094
+ ],
5095
+ managing: [
5096
+ "To customize an existing dashboard, call get_dashboard, edit the widgets array, and send the full array to update_dashboard (widgets are replaced, not merged).",
5097
+ "To start from a working layout, duplicate_dashboard a good one, then update_dashboard to tweak it.",
5098
+ "share_dashboard returns a public token; chain it after the dashboard is built."
5099
+ ],
5100
+ examples: DASHBOARD_EXAMPLES
5101
+ };
5102
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
5103
+ }
5104
+ );
5105
+ registerSpeakTool(
5106
+ server,
5107
+ "create_dashboard",
5108
+ "Create an analytics dashboard. Only `title` is required. Add widgets by listing their types (the MCP assigns ids and lays them out automatically) and scope with folderScope, dateRange, and filters. Call list_dashboard_widgets first to see the available widget types and their config keys, plus a full example.",
5109
+ {
5110
+ title: import_zod17.z.string().min(1).max(200).describe("Dashboard name (the only required field)"),
5111
+ ...writeFields
5112
+ },
5113
+ {
5114
+ title: "Create Dashboard",
5115
+ readOnlyHint: false,
5116
+ destructiveHint: false,
5117
+ idempotentHint: false,
5118
+ openWorldHint: false
5119
+ },
5120
+ async (body) => {
5121
+ try {
5122
+ const result = await api.post("/v1/dashboards", withBuiltWidgets(body));
5123
+ return {
5124
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
5125
+ };
5126
+ } catch (err) {
5127
+ return {
5128
+ content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
5129
+ isError: true
5130
+ };
5131
+ }
5132
+ }
5133
+ );
5134
+ registerSpeakTool(
5135
+ server,
5136
+ "update_dashboard",
5137
+ "Update a dashboard. Only the fields you pass are changed (partial update). NOTE: passing `widgets` replaces the entire widget set \u2014 fetch the dashboard first if you want to preserve existing widgets.",
5138
+ {
5139
+ dashboardId: import_zod17.z.string().min(1).describe("Dashboard business id"),
5140
+ title: import_zod17.z.string().min(1).max(200).optional().describe("New dashboard name"),
5141
+ ...writeFields
5142
+ },
5143
+ {
5144
+ title: "Update Dashboard",
5145
+ readOnlyHint: false,
5146
+ destructiveHint: false,
5147
+ idempotentHint: true,
5148
+ openWorldHint: false
5149
+ },
5150
+ async ({ dashboardId, ...body }) => {
4113
5151
  try {
4114
- const url = clipId ? `/v1/clips/${clipId}` : "/v1/clips";
4115
- const result = await api.get(url, { params });
5152
+ const result = await api.put(`/v1/dashboards/${dashboardId}`, withBuiltWidgets(body));
4116
5153
  return {
4117
5154
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4118
5155
  };
@@ -4126,24 +5163,21 @@ function register13(server, client) {
4126
5163
  );
4127
5164
  registerSpeakTool(
4128
5165
  server,
4129
- "update_clip",
4130
- "Update a clip's title, description, or tags.",
5166
+ "delete_dashboard",
5167
+ "Soft-delete a dashboard. This also deactivates its public share link.",
4131
5168
  {
4132
- clipId: import_zod14.z.string().min(1).describe("ID of the clip to update"),
4133
- title: import_zod14.z.string().optional().describe("New title"),
4134
- description: import_zod14.z.string().optional().describe("New description"),
4135
- tags: import_zod14.z.array(import_zod14.z.string()).optional().describe("New tags")
5169
+ dashboardId: import_zod17.z.string().min(1).describe("Dashboard business id to delete")
4136
5170
  },
4137
5171
  {
4138
- title: "Update Clip",
5172
+ title: "Delete Dashboard",
4139
5173
  readOnlyHint: false,
4140
- destructiveHint: false,
5174
+ destructiveHint: true,
4141
5175
  idempotentHint: true,
4142
5176
  openWorldHint: false
4143
5177
  },
4144
- async ({ clipId, ...body }) => {
5178
+ async ({ dashboardId }) => {
4145
5179
  try {
4146
- const result = await api.put(`/v1/clips/${clipId}`, body);
5180
+ const result = await api.delete(`/v1/dashboards/${dashboardId}`);
4147
5181
  return {
4148
5182
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4149
5183
  };
@@ -4157,21 +5191,21 @@ function register13(server, client) {
4157
5191
  );
4158
5192
  registerSpeakTool(
4159
5193
  server,
4160
- "delete_clip",
4161
- "Permanently delete a clip and its associated media file.",
5194
+ "duplicate_dashboard",
5195
+ 'Clone an existing dashboard. The copy gets fresh widget ids, a "<name> (copy)" title, and cleared sharing. Ideal for cloning a fully-configured dashboard, then tweaking it via update_dashboard.',
4162
5196
  {
4163
- clipId: import_zod14.z.string().min(1).describe("ID of the clip to delete")
5197
+ dashboardId: import_zod17.z.string().min(1).describe("Source dashboard business id to clone")
4164
5198
  },
4165
5199
  {
4166
- title: "Delete Clip",
5200
+ title: "Duplicate Dashboard",
4167
5201
  readOnlyHint: false,
4168
- destructiveHint: true,
4169
- idempotentHint: true,
5202
+ destructiveHint: false,
5203
+ idempotentHint: false,
4170
5204
  openWorldHint: false
4171
5205
  },
4172
- async ({ clipId }) => {
5206
+ async ({ dashboardId }) => {
4173
5207
  try {
4174
- const result = await api.delete(`/v1/clips/${clipId}`);
5208
+ const result = await api.post(`/v1/dashboards/${dashboardId}/duplicate`, {});
4175
5209
  return {
4176
5210
  content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4177
5211
  };
@@ -4183,110 +5217,25 @@ function register13(server, client) {
4183
5217
  }
4184
5218
  }
4185
5219
  );
4186
- }
4187
- var import_zod14;
4188
- var init_clips = __esm({
4189
- "src/tools/clips.ts"() {
4190
- "use strict";
4191
- import_zod14 = require("zod");
4192
- init_helpers();
4193
- init_client();
4194
- init_dist();
4195
- }
4196
- });
4197
-
4198
- // src/media-utils.ts
4199
- function isVideoFile(filePath) {
4200
- return VIDEO_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
4201
- }
4202
- function getMimeType(filePath) {
4203
- const ext = path.extname(filePath).toLowerCase();
4204
- const isVideo = isVideoFile(filePath);
4205
- if (ext === ".mp4") return isVideo ? "video/mp4" : "audio/mp4";
4206
- if (ext === ".webm") return isVideo ? "video/webm" : "audio/webm";
4207
- return MIME_TYPES[ext] ?? (isVideo ? "video/mp4" : "audio/mpeg");
4208
- }
4209
- function detectMediaType(filePath) {
4210
- return isVideoFile(filePath) ? "video" : "audio";
4211
- }
4212
- var path, VIDEO_EXTENSIONS, MIME_TYPES;
4213
- var init_media_utils = __esm({
4214
- "src/media-utils.ts"() {
4215
- "use strict";
4216
- path = __toESM(require("path"));
4217
- VIDEO_EXTENSIONS = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".wmv"];
4218
- MIME_TYPES = {
4219
- ".mp3": "audio/mpeg",
4220
- ".m4a": "audio/mp4",
4221
- ".wav": "audio/wav",
4222
- ".ogg": "audio/ogg",
4223
- ".flac": "audio/flac",
4224
- ".mov": "video/quicktime",
4225
- ".avi": "video/x-msvideo",
4226
- ".mkv": "video/x-matroska",
4227
- ".wmv": "video/x-ms-wmv"
4228
- };
4229
- }
4230
- });
4231
-
4232
- // src/tools/workflows.ts
4233
- var workflows_exports = {};
4234
- __export(workflows_exports, {
4235
- register: () => register14
4236
- });
4237
- function register14(server, client) {
4238
- const api = client ?? speakClient;
4239
5220
  registerSpeakTool(
4240
5221
  server,
4241
- "upload_and_analyze",
4242
- "Upload and transcribe media from a URL \u2014 a direct/public file URL, OR a shareable social/video link (YouTube, Instagram, TikTok, X, Facebook, Reddit, SoundCloud, and similar), which Speak resolves to the underlying media automatically. Returns media_id immediately; after this returns, poll get_media_status until state is 'processed' (typically 1-3 min for under 60min audio), then call get_media_insights for AI summaries. This async pattern is required for remote MCP transports \u2014 long blocking calls die at proxy idle timeouts. (Vimeo links are not yet supported.)",
5222
+ "share_dashboard",
5223
+ "Enable public sharing for a dashboard and return its share token + embed id. WARNING: by default the public link resolves with no passphrase, so anyone with the token can view the dashboard data until an owner sets one.",
4243
5224
  {
4244
- url: import_zod15.z.string().describe("Direct/public media file URL, or a shareable social/video page link (e.g. an Instagram reel, TikTok, YouTube, or X post URL) \u2014 page links are resolved to the underlying media server-side. Pass the URL the user gave you as-is."),
4245
- name: import_zod15.z.string().optional().describe("Display name for the media (defaults to filename from URL)"),
4246
- mediaType: import_zod15.z.enum([MediaType.AUDIO, MediaType.VIDEO]).optional().describe("Media type (default: audio)"),
4247
- sourceLanguage: import_zod15.z.string().optional().describe("BCP-47 language code (e.g., 'en-US', 'he-IL')"),
4248
- folderId: import_zod15.z.string().optional().describe("Folder ID to place the media in"),
4249
- tags: import_zod15.z.string().optional().describe("Comma-separated tags")
5225
+ dashboardId: import_zod17.z.string().min(1).describe("Dashboard business id to share")
4250
5226
  },
4251
5227
  {
4252
- title: "Upload and Analyze Media",
5228
+ title: "Share Dashboard",
4253
5229
  readOnlyHint: false,
4254
5230
  destructiveHint: false,
4255
- idempotentHint: false,
5231
+ idempotentHint: true,
4256
5232
  openWorldHint: false
4257
5233
  },
4258
- async (params) => {
5234
+ async ({ dashboardId }) => {
4259
5235
  try {
4260
- const uploadBody = {
4261
- name: params.name ?? params.url.split("/").pop()?.split("?")[0] ?? "Upload",
4262
- url: params.url,
4263
- mediaType: params.mediaType ?? "audio"
4264
- };
4265
- if (params.sourceLanguage) uploadBody.sourceLanguage = params.sourceLanguage;
4266
- if (params.folderId) uploadBody.folderId = params.folderId;
4267
- if (params.tags) uploadBody.tags = params.tags;
4268
- const uploadRes = await api.post("/v1/media/upload", uploadBody);
4269
- const mediaId = uploadRes.data?.data?.mediaId;
4270
- const state = uploadRes.data?.data?.state ?? "pending";
4271
- if (!mediaId) {
4272
- return {
4273
- content: [{ type: "text", text: `Error: Upload succeeded but no mediaId returned.
4274
- ${JSON.stringify(uploadRes.data, null, 2)}` }],
4275
- isError: true
4276
- };
4277
- }
4278
- const result = {
4279
- mediaId,
4280
- state,
4281
- message: "Upload accepted. Processing has started in the background.",
4282
- nextSteps: [
4283
- `1. Poll get_media_status with mediaId="${mediaId}" every 10-30 seconds.`,
4284
- `2. When state is "processed" (typically 1-3 min for audio under 60 min), call get_media_insights for the AI summary and get_transcript for the full transcript.`,
4285
- `3. If state becomes "failed", processing did not complete \u2014 surface the error to the user.`
4286
- ]
4287
- };
5236
+ const result = await api.put(`/v1/dashboards/${dashboardId}/share`, {});
4288
5237
  return {
4289
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
5238
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4290
5239
  };
4291
5240
  } catch (err) {
4292
5241
  return {
@@ -4298,88 +5247,21 @@ ${JSON.stringify(uploadRes.data, null, 2)}` }],
4298
5247
  );
4299
5248
  registerSpeakTool(
4300
5249
  server,
4301
- "upload_local_file",
4302
- [
4303
- "Upload a local file to Speak AI for transcription and analysis.",
4304
- "Reads the file from disk, gets a pre-signed S3 URL, uploads the file, then creates the media entry.",
4305
- "Works with any audio or video file on the local filesystem.",
4306
- "After upload, use get_media_status to poll for completion, then get_transcript and get_media_insights."
4307
- ].join(" "),
4308
- {
4309
- filePath: import_zod15.z.string().describe("Absolute path to the local audio or video file"),
4310
- name: import_zod15.z.string().optional().describe("Display name (defaults to filename)"),
4311
- mediaType: import_zod15.z.enum([MediaType.AUDIO, MediaType.VIDEO]).optional().describe("Media type (auto-detected from extension if omitted)"),
4312
- sourceLanguage: import_zod15.z.string().optional().describe("BCP-47 language code (e.g., 'en-US')"),
4313
- folderId: import_zod15.z.string().optional().describe("Folder ID to place the media in"),
4314
- tags: import_zod15.z.string().optional().describe("Comma-separated tags")
4315
- },
5250
+ "get_dashboard_speakers_insight",
5251
+ "Compute a speakers breakdown for a given folder scope, date range, and field filters. Standalone analytics \u2014 does not require a dashboard to exist.",
5252
+ SPEAKERS_FILTER_SCHEMA,
4316
5253
  {
4317
- title: "Upload Local File",
4318
- readOnlyHint: false,
5254
+ title: "Get Dashboard Speakers Insight",
5255
+ readOnlyHint: true,
4319
5256
  destructiveHint: false,
4320
- idempotentHint: false,
5257
+ idempotentHint: true,
4321
5258
  openWorldHint: false
4322
5259
  },
4323
- async (params) => {
5260
+ async (body) => {
4324
5261
  try {
4325
- const filePath = params.filePath;
4326
- if (!fs.existsSync(filePath)) {
4327
- return {
4328
- content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
4329
- isError: true
4330
- };
4331
- }
4332
- const filename = path2.basename(filePath);
4333
- const isVideo = isVideoFile(filePath);
4334
- const mediaType = params.mediaType ?? detectMediaType(filePath);
4335
- const mimeType = getMimeType(filePath);
4336
- const signedRes = await api.get("/v1/media/upload/signedurl", {
4337
- params: { isVideo, filename, mimeType }
4338
- });
4339
- const signedData = signedRes.data?.data;
4340
- const uploadUrl = signedData?.preSignedUrl ?? signedData?.signedUrl ?? signedData?.url;
4341
- if (!uploadUrl) {
4342
- return {
4343
- content: [{ type: "text", text: `Error: Could not get signed upload URL.
4344
- ${JSON.stringify(signedRes.data, null, 2)}` }],
4345
- isError: true
4346
- };
4347
- }
4348
- const fileBuffer = fs.readFileSync(filePath);
4349
- const axios2 = (await import("axios")).default;
4350
- await axios2.put(uploadUrl, fileBuffer, {
4351
- headers: {
4352
- "Content-Type": mimeType
4353
- },
4354
- maxBodyLength: Infinity,
4355
- maxContentLength: Infinity
4356
- });
4357
- const createBody = {
4358
- name: params.name ?? filename,
4359
- url: uploadUrl.split("?")[0],
4360
- // S3 URL without query params; server re-signs via CloudFront
4361
- mediaType
4362
- };
4363
- if (params.sourceLanguage) createBody.sourceLanguage = params.sourceLanguage;
4364
- if (params.folderId) createBody.folderId = params.folderId;
4365
- if (params.tags) createBody.tags = params.tags;
4366
- const createRes = await api.post("/v1/media/upload", createBody);
4367
- const data = createRes.data?.data;
5262
+ const result = await api.post("/v1/dashboards/insights/speakers", body);
4368
5263
  return {
4369
- content: [
4370
- {
4371
- type: "text",
4372
- text: JSON.stringify(
4373
- {
4374
- mediaId: data?.mediaId,
4375
- state: data?.state,
4376
- message: `File uploaded successfully. Use get_media_status to poll until state is 'processed', then use get_transcript and get_media_insights.`
4377
- },
4378
- null,
4379
- 2
4380
- )
4381
- }
4382
- ]
5264
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
4383
5265
  };
4384
5266
  } catch (err) {
4385
5267
  return {
@@ -4390,17 +5272,53 @@ ${JSON.stringify(signedRes.data, null, 2)}` }],
4390
5272
  }
4391
5273
  );
4392
5274
  }
4393
- var import_zod15, fs, path2;
4394
- var init_workflows = __esm({
4395
- "src/tools/workflows.ts"() {
5275
+ var import_zod17, FILTER_LIST_DESCRIPTION, widgetInputSchema, writeFields, SPEAKERS_FILTER_SCHEMA;
5276
+ var init_dashboards = __esm({
5277
+ "src/tools/dashboards.ts"() {
4396
5278
  "use strict";
4397
- import_zod15 = require("zod");
5279
+ import_zod17 = require("zod");
4398
5280
  init_helpers();
4399
5281
  init_client();
4400
- init_dist();
4401
- fs = __toESM(require("fs"));
4402
- path2 = __toESM(require("path"));
4403
- init_media_utils();
5282
+ init_dashboard_widgets();
5283
+ FILTER_LIST_DESCRIPTION = "Field filters. filters.filterList is an array of { fieldName, fieldOperator?, fieldValue?: string[], fieldCondition? }. Other keys pass through but only filterList is enforced.";
5284
+ widgetInputSchema = import_zod17.z.object({
5285
+ type: import_zod17.z.enum(WIDGET_TYPES).describe(
5286
+ "Widget type: stat-cards | sentiment | media-list | field-distribution | upload-timeline | themes | team-activity | kpi-trend | field-metric | metric-group | notes | people | comparison | sentiment-trend"
5287
+ ),
5288
+ title: import_zod17.z.string().max(200).optional().describe("Widget title (defaults to a per-type label)"),
5289
+ config: import_zod17.z.record(import_zod17.z.unknown()).optional().describe(
5290
+ "Optional render settings. Per-type: fieldId+measure (field-distribution), fieldId+aggregator (field-metric), metrics list (stat-cards, kpi-trend, comparison, metric-group), chartType (themes), heading+content (notes). Any widget also accepts accentColor (hex) and scopeOverride ({datePreset, folderId}). Call list_dashboard_widgets for the full catalog + examples. Leave empty for sensible defaults."
5291
+ )
5292
+ });
5293
+ writeFields = {
5294
+ description: import_zod17.z.string().max(2e3).optional().describe("Dashboard description"),
5295
+ icon: import_zod17.z.string().max(200).optional().describe("Icon identifier"),
5296
+ folderScope: import_zod17.z.array(import_zod17.z.string().max(100)).max(100).optional().describe("Folder ids to scope analytics to. Empty/omitted = all accessible folders."),
5297
+ assignTo: import_zod17.z.array(import_zod17.z.string()).max(100).optional().describe('User ids, or group ids in the "<groupId> (G)" convention, to share view access with'),
5298
+ dateRange: import_zod17.z.object({
5299
+ preset: import_zod17.z.string().max(50).optional(),
5300
+ startDate: import_zod17.z.string().optional(),
5301
+ endDate: import_zod17.z.string().optional()
5302
+ }).optional().describe("Date range: { preset?, startDate?, endDate? }"),
5303
+ filters: import_zod17.z.record(import_zod17.z.unknown()).optional().describe(FILTER_LIST_DESCRIPTION),
5304
+ widgets: import_zod17.z.array(widgetInputSchema).max(50).optional().describe(
5305
+ "Widgets to place on the dashboard, in order. The MCP assigns ids and computes a tidy two-per-row grid layout matching the Speak UI. Just list the widget types you want."
5306
+ ),
5307
+ isDefault: import_zod17.z.boolean().optional().describe("Make this the company default dashboard")
5308
+ };
5309
+ SPEAKERS_FILTER_SCHEMA = {
5310
+ folderScope: import_zod17.z.array(import_zod17.z.string().max(100)).max(100).optional().describe("Folder ids to scope to"),
5311
+ startDate: import_zod17.z.string().optional().describe("ISO start date"),
5312
+ endDate: import_zod17.z.string().optional().describe("ISO end date"),
5313
+ filterList: import_zod17.z.array(
5314
+ import_zod17.z.object({
5315
+ fieldName: import_zod17.z.string().max(100),
5316
+ fieldOperator: import_zod17.z.string().max(50).optional(),
5317
+ fieldValue: import_zod17.z.array(import_zod17.z.string().max(500)).optional(),
5318
+ fieldCondition: import_zod17.z.string().max(50).optional()
5319
+ })
5320
+ ).max(20).optional().describe("Field filter rules")
5321
+ };
4404
5322
  }
4405
5323
  });
4406
5324
 
@@ -4432,6 +5350,8 @@ var init_tools = __esm({
4432
5350
  init_analytics();
4433
5351
  init_clips();
4434
5352
  init_workflows();
5353
+ init_users();
5354
+ init_dashboards();
4435
5355
  modules = [
4436
5356
  media_exports,
4437
5357
  text_exports,
@@ -4446,7 +5366,9 @@ var init_tools = __esm({
4446
5366
  webhooks_exports,
4447
5367
  analytics_exports,
4448
5368
  clips_exports,
4449
- workflows_exports
5369
+ workflows_exports,
5370
+ users_exports,
5371
+ dashboards_exports
4450
5372
  ];
4451
5373
  }
4452
5374
  });
@@ -4563,8 +5485,8 @@ function registerPrompts(server) {
4563
5485
  "analyze-meeting",
4564
5486
  "Upload a meeting recording and get a full analysis \u2014 transcript, insights, action items, and key takeaways.",
4565
5487
  {
4566
- url: import_zod16.z.string().describe("URL of the meeting recording \u2014 a direct file link or a shareable social/video link (resolved automatically)"),
4567
- name: import_zod16.z.string().optional().describe("Meeting name (optional)")
5488
+ url: import_zod18.z.string().describe("URL of the meeting recording \u2014 a direct file link or a shareable social/video link (resolved automatically)"),
5489
+ name: import_zod18.z.string().optional().describe("Meeting name (optional)")
4568
5490
  },
4569
5491
  async ({ url, name }) => ({
4570
5492
  messages: [
@@ -4596,8 +5518,8 @@ function registerPrompts(server) {
4596
5518
  "research-across-media",
4597
5519
  "Search for themes, patterns, or topics across multiple recordings or your entire media library.",
4598
5520
  {
4599
- topic: import_zod16.z.string().describe("The topic, theme, or question to research"),
4600
- folder: import_zod16.z.string().optional().describe("Folder ID to scope the research (optional)")
5521
+ topic: import_zod18.z.string().describe("The topic, theme, or question to research"),
5522
+ folder: import_zod18.z.string().optional().describe("Folder ID to scope the research (optional)")
4601
5523
  },
4602
5524
  async ({ topic, folder }) => ({
4603
5525
  messages: [
@@ -4630,8 +5552,8 @@ function registerPrompts(server) {
4630
5552
  "meeting-brief",
4631
5553
  "Prepare a brief from recent meetings \u2014 pull transcripts, extract decisions, and summarize open items.",
4632
5554
  {
4633
- days: import_zod16.z.string().optional().describe("Number of days to look back (default: 7)"),
4634
- folder: import_zod16.z.string().optional().describe("Folder ID to scope to (optional)")
5555
+ days: import_zod18.z.string().optional().describe("Number of days to look back (default: 7)"),
5556
+ folder: import_zod18.z.string().optional().describe("Folder ID to scope to (optional)")
4635
5557
  },
4636
5558
  async ({ days, folder }) => {
4637
5559
  const lookback = parseInt(days ?? "7");
@@ -4668,11 +5590,148 @@ function registerPrompts(server) {
4668
5590
  }
4669
5591
  );
4670
5592
  }
4671
- var import_zod16;
5593
+ var import_zod18;
4672
5594
  var init_prompts = __esm({
4673
5595
  "src/prompts.ts"() {
4674
5596
  "use strict";
4675
- import_zod16 = require("zod");
5597
+ import_zod18 = require("zod");
5598
+ }
5599
+ });
5600
+
5601
+ // src/tool-names.ts
5602
+ var tool_names_exports = {};
5603
+ __export(tool_names_exports, {
5604
+ SPEAK_MCP_TOOL_NAMES: () => SPEAK_MCP_TOOL_NAMES
5605
+ });
5606
+ var SPEAK_MCP_TOOL_NAMES;
5607
+ var init_tool_names = __esm({
5608
+ "src/tool-names.ts"() {
5609
+ "use strict";
5610
+ SPEAK_MCP_TOOL_NAMES = [
5611
+ // analytics
5612
+ "get_media_statistics",
5613
+ // automations
5614
+ "list_automations",
5615
+ "list_automation_names",
5616
+ "get_automation",
5617
+ "get_automation_runs",
5618
+ "create_automation",
5619
+ "update_automation",
5620
+ "toggle_automation_status",
5621
+ "bulk_update_automation_status",
5622
+ "bulk_assign_automation_folders",
5623
+ "run_automations",
5624
+ "delete_automation",
5625
+ "list_automation_apps",
5626
+ "list_automation_triggers",
5627
+ "list_automation_actions",
5628
+ // clips
5629
+ "get_clips",
5630
+ "create_clip",
5631
+ "update_clip",
5632
+ "delete_clip",
5633
+ // embed
5634
+ "create_embed",
5635
+ "update_embed",
5636
+ "check_embed",
5637
+ "get_embed_iframe_url",
5638
+ // exports
5639
+ "export_media",
5640
+ "export_multiple_media",
5641
+ // fields
5642
+ "list_fields",
5643
+ "create_field",
5644
+ "update_field",
5645
+ "update_multiple_fields",
5646
+ // folders
5647
+ "list_folders",
5648
+ "create_folder",
5649
+ "update_folder",
5650
+ "delete_folder",
5651
+ "get_folder_info",
5652
+ "clone_folder",
5653
+ "get_folder_views",
5654
+ "get_all_folder_views",
5655
+ "create_folder_view",
5656
+ "update_folder_view",
5657
+ "clone_folder_view",
5658
+ // media
5659
+ "get_signed_upload_url",
5660
+ "upload_media",
5661
+ "get_media_status",
5662
+ "get_media_insights",
5663
+ "get_transcript",
5664
+ "list_media",
5665
+ "search_media",
5666
+ "delete_media",
5667
+ "update_media_metadata",
5668
+ "toggle_media_favorite",
5669
+ "reanalyze_media",
5670
+ "get_captions",
5671
+ "list_supported_languages",
5672
+ "update_transcript_speakers",
5673
+ "bulk_update_transcript_speakers",
5674
+ "bulk_move_media",
5675
+ // meeting
5676
+ "list_meeting_events",
5677
+ "schedule_meeting_event",
5678
+ "remove_assistant_from_meeting",
5679
+ "delete_scheduled_assistant",
5680
+ "get_live_meeting_transcript",
5681
+ // prompt
5682
+ "ask_magic_prompt",
5683
+ "list_prompts",
5684
+ "get_favorite_prompts",
5685
+ "toggle_prompt_favorite",
5686
+ "get_chat_history",
5687
+ "get_chat_messages",
5688
+ "update_chat_title",
5689
+ "delete_chat_message",
5690
+ "submit_chat_feedback",
5691
+ "retry_magic_prompt",
5692
+ "export_chat_answer",
5693
+ "get_chat_statistics",
5694
+ // recorder
5695
+ "list_recorders",
5696
+ "create_recorder",
5697
+ "update_recorder_settings",
5698
+ "update_recorder_questions",
5699
+ "delete_recorder",
5700
+ "generate_recorder_url",
5701
+ "get_recorder_info",
5702
+ "get_recorder_recordings",
5703
+ "check_recorder_status",
5704
+ "clone_recorder",
5705
+ // text
5706
+ "create_text_note",
5707
+ "update_text_note",
5708
+ "get_text_insight",
5709
+ "reanalyze_text",
5710
+ // webhooks
5711
+ "list_webhooks",
5712
+ "create_webhook",
5713
+ "update_webhook",
5714
+ "delete_webhook",
5715
+ // workflows (high-level wrappers around media + upload tools)
5716
+ "upload_and_analyze",
5717
+ "upload_local_file",
5718
+ // users / team management
5719
+ "list_users",
5720
+ "list_user_groups",
5721
+ "create_user_group",
5722
+ "update_user_group",
5723
+ "delete_user_group",
5724
+ // dashboards
5725
+ "list_dashboard_widgets",
5726
+ "list_dashboards",
5727
+ "get_dashboard",
5728
+ "create_dashboard",
5729
+ "update_dashboard",
5730
+ "delete_dashboard",
5731
+ "duplicate_dashboard",
5732
+ "share_dashboard",
5733
+ "get_dashboard_speakers_insight"
5734
+ ];
4676
5735
  }
4677
5736
  });
4678
5737
 
@@ -5719,8 +6778,71 @@ function createCli() {
5719
6778
  process.exit(1);
5720
6779
  }
5721
6780
  });
6781
+ async function loadToolHandlers() {
6782
+ const client = await getClient();
6783
+ const handlers = {};
6784
+ const stub = {
6785
+ registerTool: (name, _def, cb) => {
6786
+ handlers[name] = cb;
6787
+ return {};
6788
+ }
6789
+ };
6790
+ const { registerAllTools: registerAllTools2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
6791
+ registerAllTools2(stub, client);
6792
+ return handlers;
6793
+ }
6794
+ program.command("tools").description("List every MCP tool callable via `call`").option("--json", "Output raw JSON").action(async (opts) => {
6795
+ const { SPEAK_MCP_TOOL_NAMES: SPEAK_MCP_TOOL_NAMES2 } = await Promise.resolve().then(() => (init_tool_names(), tool_names_exports));
6796
+ const names = [...SPEAK_MCP_TOOL_NAMES2].sort();
6797
+ if (opts.json) {
6798
+ printJson(names);
6799
+ } else {
6800
+ console.log(`${names.length} tools:
6801
+ `);
6802
+ for (const n of names) console.log(` ${n}`);
6803
+ }
6804
+ });
6805
+ program.command("call").description("Call any MCP tool by name with JSON arguments").argument("<tool>", "Tool name (see `speakai-mcp tools`)").argument("[json]", "Arguments as a JSON object", "{}").action(async (tool, json) => {
6806
+ requireApiKey();
6807
+ let args2;
6808
+ try {
6809
+ args2 = JSON.parse(json);
6810
+ } catch {
6811
+ printError(`Invalid JSON arguments: ${json}`);
6812
+ process.exit(1);
6813
+ return;
6814
+ }
6815
+ const handlers = await loadToolHandlers();
6816
+ const handler = handlers[tool];
6817
+ if (!handler) {
6818
+ printError(`Unknown tool "${tool}". Run "speakai-mcp tools" to list them.`);
6819
+ process.exit(1);
6820
+ return;
6821
+ }
6822
+ try {
6823
+ const result = await handler(args2);
6824
+ const text = result?.content?.find((c) => c.type === "text")?.text;
6825
+ if (result?.isError) {
6826
+ printError(text ?? "Tool call failed");
6827
+ process.exit(1);
6828
+ return;
6829
+ }
6830
+ const data = result?.structuredContent?.data ?? (text ? safeParse(text) : result);
6831
+ printJson(data);
6832
+ } catch (err) {
6833
+ printError(err.response?.data?.message ?? err.message);
6834
+ process.exit(1);
6835
+ }
6836
+ });
5722
6837
  return program;
5723
6838
  }
6839
+ function safeParse(text) {
6840
+ try {
6841
+ return JSON.parse(text);
6842
+ } catch {
6843
+ return text;
6844
+ }
6845
+ }
5724
6846
  var import_commander, import_readline;
5725
6847
  var init_cli = __esm({
5726
6848
  "src/cli/index.ts"() {
@@ -5748,110 +6870,7 @@ init_tools();
5748
6870
  init_resources();
5749
6871
  init_prompts();
5750
6872
  init_client();
5751
-
5752
- // src/tool-names.ts
5753
- var SPEAK_MCP_TOOL_NAMES = [
5754
- // analytics
5755
- "get_media_statistics",
5756
- // automations
5757
- "list_automations",
5758
- "get_automation",
5759
- "create_automation",
5760
- "update_automation",
5761
- "toggle_automation_status",
5762
- // clips
5763
- "get_clips",
5764
- "create_clip",
5765
- "update_clip",
5766
- "delete_clip",
5767
- // embed
5768
- "create_embed",
5769
- "update_embed",
5770
- "check_embed",
5771
- "get_embed_iframe_url",
5772
- // exports
5773
- "export_media",
5774
- "export_multiple_media",
5775
- // fields
5776
- "list_fields",
5777
- "create_field",
5778
- "update_field",
5779
- "update_multiple_fields",
5780
- // folders
5781
- "list_folders",
5782
- "create_folder",
5783
- "update_folder",
5784
- "delete_folder",
5785
- "get_folder_info",
5786
- "clone_folder",
5787
- "get_folder_views",
5788
- "get_all_folder_views",
5789
- "create_folder_view",
5790
- "update_folder_view",
5791
- "clone_folder_view",
5792
- // media
5793
- "get_signed_upload_url",
5794
- "upload_media",
5795
- "get_media_status",
5796
- "get_media_insights",
5797
- "get_transcript",
5798
- "list_media",
5799
- "search_media",
5800
- "delete_media",
5801
- "update_media_metadata",
5802
- "toggle_media_favorite",
5803
- "reanalyze_media",
5804
- "get_captions",
5805
- "list_supported_languages",
5806
- "update_transcript_speakers",
5807
- "bulk_update_transcript_speakers",
5808
- "bulk_move_media",
5809
- // meeting
5810
- "list_meeting_events",
5811
- "schedule_meeting_event",
5812
- "remove_assistant_from_meeting",
5813
- "delete_scheduled_assistant",
5814
- "get_live_meeting_transcript",
5815
- // prompt
5816
- "ask_magic_prompt",
5817
- "list_prompts",
5818
- "get_favorite_prompts",
5819
- "toggle_prompt_favorite",
5820
- "get_chat_history",
5821
- "get_chat_messages",
5822
- "update_chat_title",
5823
- "delete_chat_message",
5824
- "submit_chat_feedback",
5825
- "retry_magic_prompt",
5826
- "export_chat_answer",
5827
- "get_chat_statistics",
5828
- // recorder
5829
- "list_recorders",
5830
- "create_recorder",
5831
- "update_recorder_settings",
5832
- "update_recorder_questions",
5833
- "delete_recorder",
5834
- "generate_recorder_url",
5835
- "get_recorder_info",
5836
- "get_recorder_recordings",
5837
- "check_recorder_status",
5838
- "clone_recorder",
5839
- // text
5840
- "create_text_note",
5841
- "update_text_note",
5842
- "get_text_insight",
5843
- "reanalyze_text",
5844
- // webhooks
5845
- "list_webhooks",
5846
- "create_webhook",
5847
- "update_webhook",
5848
- "delete_webhook",
5849
- // workflows (high-level wrappers around media + upload tools)
5850
- "upload_and_analyze",
5851
- "upload_local_file"
5852
- ];
5853
-
5854
- // src/index.ts
6873
+ init_tool_names();
5855
6874
  var args = process.argv.slice(2);
5856
6875
  var cliCommands = [
5857
6876
  "config",