@speakai/mcp-server 1.13.8 → 1.14.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.
- package/dist/index.js +558 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2449,7 +2449,7 @@ function register5(server, client) {
|
|
|
2449
2449
|
name: import_zod6.z.string().describe("Display name for the recorder"),
|
|
2450
2450
|
...recorderConfigShape,
|
|
2451
2451
|
clientInformation: import_zod6.z.record(import_zod6.z.unknown()).optional().describe(
|
|
2452
|
-
|
|
2452
|
+
`Respondent info & questions: { name:boolean, email:boolean, questions:[\u2026], consent?:{ isEnabled, title, description, yesButtonLabel, noButtonLabel, isRequired, fieldId? } }. Question shape \u2014 ${QUESTION_SHAPE_DESC}`
|
|
2453
2453
|
)
|
|
2454
2454
|
},
|
|
2455
2455
|
{
|
|
@@ -2661,7 +2661,7 @@ function register5(server, client) {
|
|
|
2661
2661
|
name: import_zod6.z.boolean().optional().describe("Whether to collect the respondent's name"),
|
|
2662
2662
|
email: import_zod6.z.boolean().optional().describe("Whether to collect the respondent's email"),
|
|
2663
2663
|
questions: import_zod6.z.array(import_zod6.z.record(import_zod6.z.unknown())).describe(
|
|
2664
|
-
|
|
2664
|
+
`Survey questions. ${QUESTION_SHAPE_DESC} (id? may also be passed to update an existing question.)`
|
|
2665
2665
|
),
|
|
2666
2666
|
consent: import_zod6.z.record(import_zod6.z.unknown()).optional().describe(
|
|
2667
2667
|
"Consent screen: { isEnabled, title, description, yesButtonLabel, noButtonLabel, isRequired, fieldId? }"
|
|
@@ -2717,13 +2717,24 @@ function register5(server, client) {
|
|
|
2717
2717
|
}
|
|
2718
2718
|
);
|
|
2719
2719
|
}
|
|
2720
|
-
var import_zod6, recorderConfigShape;
|
|
2720
|
+
var import_zod6, RECORDER_ANSWER_TYPES, QUESTION_SHAPE_DESC, recorderConfigShape;
|
|
2721
2721
|
var init_recorder3 = __esm({
|
|
2722
2722
|
"src/tools/recorder.ts"() {
|
|
2723
2723
|
"use strict";
|
|
2724
2724
|
import_zod6 = require("zod");
|
|
2725
2725
|
init_helpers();
|
|
2726
2726
|
init_client();
|
|
2727
|
+
RECORDER_ANSWER_TYPES = [
|
|
2728
|
+
"single",
|
|
2729
|
+
"multiple",
|
|
2730
|
+
"checkbox",
|
|
2731
|
+
"radiobutton",
|
|
2732
|
+
"dropdownlist",
|
|
2733
|
+
"date",
|
|
2734
|
+
"time",
|
|
2735
|
+
"datetime"
|
|
2736
|
+
];
|
|
2737
|
+
QUESTION_SHAPE_DESC = `Each: { question, isRequired, answerType, options?, includeOther?, fieldId? }. answerType must be one of: ${RECORDER_ANSWER_TYPES.map((t) => `"${t}"`).join(", ")}. Choice types (single, multiple, checkbox, radiobutton, dropdownlist) take options:string[] and includeOther:boolean (adds a free-text "Other"). date/time/datetime take no options. There is no free-text/rating/number answerType.`;
|
|
2727
2738
|
recorderConfigShape = {
|
|
2728
2739
|
description: import_zod6.z.string().optional().describe("Recorder description"),
|
|
2729
2740
|
sourceLanguage: import_zod6.z.string().optional().describe("Transcription language code (e.g. en-US)"),
|
|
@@ -3660,11 +3671,87 @@ var init_fields2 = __esm({
|
|
|
3660
3671
|
}
|
|
3661
3672
|
});
|
|
3662
3673
|
|
|
3674
|
+
// src/tools/inbound-webhook-utils.ts
|
|
3675
|
+
function unwrapData(payload) {
|
|
3676
|
+
const p = payload;
|
|
3677
|
+
return p && typeof p === "object" && "status" in p && "data" in p ? p.data : p;
|
|
3678
|
+
}
|
|
3679
|
+
function narrowPathsToChildKey(paths, childKey) {
|
|
3680
|
+
if (!childKey) return paths;
|
|
3681
|
+
const prefix = `${childKey}.`;
|
|
3682
|
+
const narrowed = paths.filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length)).filter(Boolean);
|
|
3683
|
+
return narrowed.length ? narrowed : paths;
|
|
3684
|
+
}
|
|
3685
|
+
function buildHowToUse(inboundUrl, sampleCaptured) {
|
|
3686
|
+
const url = inboundUrl ?? "<inboundUrl>";
|
|
3687
|
+
const lines = [
|
|
3688
|
+
`Send events with: curl -X POST '${url}' -H 'Content-Type: application/json' -d '{"url": "https://example.com/file.mp3", "name": "My recording"}'`,
|
|
3689
|
+
`To capture/refresh a sample payload WITHOUT running the automation, POST to '${url}?test=1'.`,
|
|
3690
|
+
"Reference payload values in step configs with {{trigger.payload.<path>}} tokens \u2014 see mappableTokens."
|
|
3691
|
+
];
|
|
3692
|
+
if (!sampleCaptured) {
|
|
3693
|
+
lines.unshift(
|
|
3694
|
+
"No sample payload captured yet \u2014 send a test request first (see below) so payload paths become discoverable for mapping, then call get_inbound_webhook again."
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3697
|
+
return lines;
|
|
3698
|
+
}
|
|
3699
|
+
async function fetchInboundWebhookInfo(api, webhookId, childKey) {
|
|
3700
|
+
const res = await api.get(`/v1/webhook/${webhookId}`);
|
|
3701
|
+
const payload = unwrapData(res.data);
|
|
3702
|
+
const wh = payload?.webhookData ?? payload ?? {};
|
|
3703
|
+
const flattened = Array.isArray(wh.flattenedPaths) ? wh.flattenedPaths : [];
|
|
3704
|
+
const sample = wh.samplePayload ?? null;
|
|
3705
|
+
const sampleCaptured = sample != null && (typeof sample !== "object" || Object.keys(sample).length > 0);
|
|
3706
|
+
const inboundUrl = typeof wh.inboundUrl === "string" ? wh.inboundUrl : null;
|
|
3707
|
+
return {
|
|
3708
|
+
webhookId,
|
|
3709
|
+
inboundUrl,
|
|
3710
|
+
sampleCaptured,
|
|
3711
|
+
samplePayload: sample,
|
|
3712
|
+
mappableTokens: narrowPathsToChildKey(flattened, childKey).map(
|
|
3713
|
+
(p) => `{{trigger.payload.${p}}}`
|
|
3714
|
+
),
|
|
3715
|
+
...childKey ? { childKey } : {},
|
|
3716
|
+
howToUse: buildHowToUse(inboundUrl, sampleCaptured)
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
async function resolveAutomationInboundWebhook(api, automationId) {
|
|
3720
|
+
const res = await api.get(`/v1/automations/${automationId}`);
|
|
3721
|
+
const automation = unwrapData(res.data);
|
|
3722
|
+
const trigger = automation?.trigger ?? {};
|
|
3723
|
+
return {
|
|
3724
|
+
webhookId: typeof trigger.webhookId === "string" && trigger.webhookId ? trigger.webhookId : void 0,
|
|
3725
|
+
childKey: typeof trigger.childKey === "string" && trigger.childKey ? trigger.childKey : void 0
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
function isInboundWebhookTrigger(trigger) {
|
|
3729
|
+
const t = trigger;
|
|
3730
|
+
return !!t && (t.triggerSlug === "inbound_webhook" || t.type === "webhook" || !!t.webhookId);
|
|
3731
|
+
}
|
|
3732
|
+
var init_inbound_webhook_utils = __esm({
|
|
3733
|
+
"src/tools/inbound-webhook-utils.ts"() {
|
|
3734
|
+
"use strict";
|
|
3735
|
+
}
|
|
3736
|
+
});
|
|
3737
|
+
|
|
3663
3738
|
// src/tools/automations.ts
|
|
3664
3739
|
var automations_exports = {};
|
|
3665
3740
|
__export(automations_exports, {
|
|
3666
3741
|
register: () => register10
|
|
3667
3742
|
});
|
|
3743
|
+
async function withInboundWebhookInfo(api, responseData, automationId) {
|
|
3744
|
+
try {
|
|
3745
|
+
if (!automationId) return responseData;
|
|
3746
|
+
const { webhookId, childKey } = await resolveAutomationInboundWebhook(api, automationId);
|
|
3747
|
+
if (!webhookId) return responseData;
|
|
3748
|
+
const inboundWebhook = await fetchInboundWebhookInfo(api, webhookId, childKey);
|
|
3749
|
+
const base = responseData && typeof responseData === "object" ? responseData : { data: responseData };
|
|
3750
|
+
return { ...base, inboundWebhook };
|
|
3751
|
+
} catch {
|
|
3752
|
+
return responseData;
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3668
3755
|
function register10(server, client) {
|
|
3669
3756
|
const api = client ?? speakClient;
|
|
3670
3757
|
registerSpeakTool(
|
|
@@ -3789,7 +3876,7 @@ function register10(server, client) {
|
|
|
3789
3876
|
registerSpeakTool(
|
|
3790
3877
|
server,
|
|
3791
3878
|
"create_automation",
|
|
3792
|
-
"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.",
|
|
3879
|
+
"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. For inbound-webhook automations the response includes inboundWebhook.inboundUrl (where to POST payloads) \u2014 recommended flow: create, send a test payload to the URL with ?test=1, call get_inbound_webhook to see mappable payload tokens, then update_automation to wire tokens/fieldsMap.",
|
|
3793
3880
|
writeSchema,
|
|
3794
3881
|
{
|
|
3795
3882
|
title: "Create Automation",
|
|
@@ -3801,8 +3888,13 @@ function register10(server, client) {
|
|
|
3801
3888
|
async (body) => {
|
|
3802
3889
|
try {
|
|
3803
3890
|
const result = await api.post("/v1/automations/", body);
|
|
3891
|
+
let data = result.data;
|
|
3892
|
+
if (isInboundWebhookTrigger(body.trigger)) {
|
|
3893
|
+
const automationId = unwrapData(result.data)?.automationId;
|
|
3894
|
+
data = await withInboundWebhookInfo(api, data, automationId);
|
|
3895
|
+
}
|
|
3804
3896
|
return {
|
|
3805
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
3897
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
3806
3898
|
};
|
|
3807
3899
|
} catch (err) {
|
|
3808
3900
|
return {
|
|
@@ -3830,8 +3922,12 @@ function register10(server, client) {
|
|
|
3830
3922
|
async ({ automationId, ...body }) => {
|
|
3831
3923
|
try {
|
|
3832
3924
|
const result = await api.put(`/v1/automations/${automationId}`, body);
|
|
3925
|
+
let data = result.data;
|
|
3926
|
+
if (isInboundWebhookTrigger(body.trigger)) {
|
|
3927
|
+
data = await withInboundWebhookInfo(api, data, automationId);
|
|
3928
|
+
}
|
|
3833
3929
|
return {
|
|
3834
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
3930
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
3835
3931
|
};
|
|
3836
3932
|
} catch (err) {
|
|
3837
3933
|
return {
|
|
@@ -4067,18 +4163,27 @@ function register10(server, client) {
|
|
|
4067
4163
|
}
|
|
4068
4164
|
);
|
|
4069
4165
|
}
|
|
4070
|
-
var import_zod11, STEPS_DESCRIPTION, TRIGGER_DESCRIPTION, writeSchema;
|
|
4166
|
+
var import_zod11, TOKEN_SYNTAX_NOTE, STEPS_DESCRIPTION, TRIGGER_DESCRIPTION, OR_TRIGGERS_DESCRIPTION, writeSchema;
|
|
4071
4167
|
var init_automations = __esm({
|
|
4072
4168
|
"src/tools/automations.ts"() {
|
|
4073
4169
|
"use strict";
|
|
4074
4170
|
import_zod11 = require("zod");
|
|
4075
4171
|
init_helpers();
|
|
4076
4172
|
init_client();
|
|
4077
|
-
|
|
4078
|
-
|
|
4173
|
+
init_inbound_webhook_utils();
|
|
4174
|
+
TOKEN_SYNTAX_NOTE = "Token syntax (usable in fields marked 'tokens allowed'): {{trigger.payload.<path>}} reads the inbound webhook payload (dot paths and [n] array indices; paths are relative to trigger.childKey when set \u2014 discover valid paths with get_inbound_webhook after sending a test payload); {{step.<index>.<path>}} or {{step.<stepId>.<path>}} reads a previous step's output (speak-upload -> mediaId, magic-prompt -> answer, outbound-webhook -> status/response).";
|
|
4175
|
+
STEPS_DESCRIPTION = 'Ordered array of graph steps (1-20). Each step is an object: { stepId: string (unique within the array), stepType: one of "speak-upload" | "magic-prompt" | "translation" | "filter" | "condition" | "notify" | "outbound-webhook" | "composio-action", dependsOn?: string[] (stepIds this step runs after), branch?: "true"|"false" (which outcome of an upstream condition step this step belongs to) } plus ONE config key matching stepType:\n- speak-upload -> speakUpload: { sourceMode: "url"|"file", sourceUrl (required when sourceMode="url"; tokens allowed \u2014 if the token resolves to an object, the first http(s) URL inside it is used), folderId (required, unless folderRouting.mode="dynamic" where it becomes the optional fallback), name? (tokens allowed, mixable with static text), language? (language code or token), fieldsMap?: { <customFieldId>: "<value>" } (writes payload values into Speak custom fields on the uploaded media; values are usually {{trigger.payload.<path>}} tokens \u2014 get field ids from list_fields), folderRouting?: { mode: "static"|"dynamic", sourceKey (payload key holding the destination folder name, required when dynamic), onNoMatch: "create"|"default" (create a folder named after the value, or fall back to folderId) } }\n- magic-prompt -> magicPrompt: { prompt (required unless fieldIds given, max 20000), title?, assistantType? ("general"|"researcher"|"marketer"|"sales"|"recruiter"|"custom", default "general"), assistantTemplateId? (required if assistantType="custom"), fieldIds?: string[] (max 10 \u2014 extract answers into these custom fields) }\n- translation -> translation: { targetLanguage: region-qualified locale code, e.g. "es-ES", "fr-FR" (bare codes like "es" are rejected) }\n- filter -> filter: { logic: "AND"|"OR" (default "AND"), rules: [{ field, op, value? }] (1-20) } \u2014 the run continues only when the rules match, otherwise it stops silently\n- condition -> condition: same { logic, rules } shape as filter, but instead of stopping it routes: downstream steps marked branch:"true"/"false" run according to the outcome\n- notify -> notify: { channel: "in_app"|"email"|"slack", target?, message (required, tokens allowed) }\n- outbound-webhook -> outboundWebhook: { url (required, tokens allowed), method? ("GET"|"POST"|"PUT"|"PATCH"|"DELETE", default "POST"), headers?: { <name>: <value> }, bodyTemplate?: string | object (tokens allowed) }\n- composio-action -> composio: { app, action, connectedAccountId?, argsTemplate? } (Composio is currently behind a server flag and may be unavailable)\nFilter/condition rule fields depend on what flows into the step: MEDIA -> name|duration|sourceLanguage|tags|transcript|speakers or a custom field id; INSIGHT -> answer; inbound-webhook DATA -> any payload path (e.g. "contact.status"). Ops by field type \u2014 text: eq|neq|contains|ncontains|startsWith|exists; number: eq|neq|gt|lt|exists; array: contains|ncontains|exists ("exists" takes no value; gt/lt values are numbers).\n' + TOKEN_SYNTAX_NOTE;
|
|
4176
|
+
TRIGGER_DESCRIPTION = `Trigger object (the automation's root). Always include triggerSlug. Supported shapes:
|
|
4177
|
+
- Media analyzed in folder(s): { type: "folders", triggerSlug: "media_analyzed", folderIds: string[] (min 1) }
|
|
4178
|
+
- Inbound webhook (receive external payloads): { type: "folders", triggerSlug: "inbound_webhook", webhookId? (from provision_inbound_webhook; omit to auto-provision a new one on create), childKey? (dot-path narrowing which part of the payload feeds the automation, e.g. "data") }. The create/update response includes inboundWebhook.inboundUrl \u2014 the public URL to POST payloads to.
|
|
4179
|
+
- Custom field updated: { type: "folders", triggerSlug: "field_updated", values: string[] (watched custom field ids, min 1), fieldValueMatches?: [{ fieldId, values: string[] }] (fire only when the field changes TO one of these values; empty values = any change), fieldMatchLogic?: "AND"|"OR" (how multiple fieldValueMatches combine, default "OR") }
|
|
4180
|
+
- Composio app event: { type: "composio", provider: "composio", app, triggerSlug, connectedAccountId } (requires a connected account; may be behind a server flag)
|
|
4181
|
+
Notes: "tags"/"keywords" trigger types are rejected for graph automations. The server stores inbound-webhook triggers with type "webhook" internally \u2014 send type "folders" plus the slug as shown above.`;
|
|
4182
|
+
OR_TRIGGERS_DESCRIPTION = 'Optional additional "Or" triggers (max 10): the automation runs when ANY of them fires, sharing the same steps. Each entry mirrors the trigger shapes above but cannot be an inbound webhook and carries no webhookId/childKey. Example: [{ type: "folders", triggerSlug: "field_updated", values: ["<fieldId>"] }]';
|
|
4079
4183
|
writeSchema = {
|
|
4080
4184
|
name: import_zod11.z.string().min(1).max(150).describe("Display name for the automation"),
|
|
4081
4185
|
trigger: import_zod11.z.record(import_zod11.z.unknown()).describe(TRIGGER_DESCRIPTION),
|
|
4186
|
+
triggers: import_zod11.z.array(import_zod11.z.record(import_zod11.z.unknown())).max(10).optional().describe(OR_TRIGGERS_DESCRIPTION),
|
|
4082
4187
|
steps: import_zod11.z.array(import_zod11.z.record(import_zod11.z.unknown())).min(1).max(20).describe(STEPS_DESCRIPTION),
|
|
4083
4188
|
description: import_zod11.z.string().max(1e3).optional().describe("Optional description"),
|
|
4084
4189
|
isActive: import_zod11.z.boolean().optional().describe("Whether the automation is active (defaults to true)"),
|
|
@@ -4184,6 +4289,126 @@ function register11(server, client) {
|
|
|
4184
4289
|
}
|
|
4185
4290
|
}
|
|
4186
4291
|
);
|
|
4292
|
+
registerSpeakTool(
|
|
4293
|
+
server,
|
|
4294
|
+
"provision_inbound_webhook",
|
|
4295
|
+
"Provision a standalone inbound webhook and get its public receive URL (inboundUrl) BEFORE creating an automation. Webhook-first flow: provision, send a test payload to the URL (append ?test=1 to only capture a sample without running anything), inspect mappable payload paths with get_inbound_webhook, then pass the webhookId as trigger.webhookId to create_automation.",
|
|
4296
|
+
{},
|
|
4297
|
+
{
|
|
4298
|
+
title: "Provision Inbound Webhook",
|
|
4299
|
+
readOnlyHint: false,
|
|
4300
|
+
destructiveHint: false,
|
|
4301
|
+
idempotentHint: false,
|
|
4302
|
+
openWorldHint: true
|
|
4303
|
+
},
|
|
4304
|
+
async () => {
|
|
4305
|
+
try {
|
|
4306
|
+
const result = await api.post("/v1/webhook/inbound/provision", {});
|
|
4307
|
+
const payload = unwrapData(result.data) ?? {};
|
|
4308
|
+
const inboundUrl = typeof payload.inboundUrl === "string" ? payload.inboundUrl : "<inboundUrl>";
|
|
4309
|
+
const data = {
|
|
4310
|
+
...payload,
|
|
4311
|
+
nextSteps: [
|
|
4312
|
+
`Capture a sample payload (does not run anything): curl -X POST '${inboundUrl}?test=1' -H 'Content-Type: application/json' -d '{"url": "https://example.com/file.mp3", "name": "Test"}'`,
|
|
4313
|
+
"Call get_inbound_webhook with this webhookId to see the captured sample and mappable {{trigger.payload.*}} tokens.",
|
|
4314
|
+
'Create the automation with create_automation, passing this webhookId in trigger.webhookId (triggerSlug: "inbound_webhook").'
|
|
4315
|
+
]
|
|
4316
|
+
};
|
|
4317
|
+
return {
|
|
4318
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
4319
|
+
};
|
|
4320
|
+
} catch (err) {
|
|
4321
|
+
return {
|
|
4322
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
4323
|
+
isError: true
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
);
|
|
4328
|
+
registerSpeakTool(
|
|
4329
|
+
server,
|
|
4330
|
+
"get_inbound_webhook",
|
|
4331
|
+
"Get an inbound webhook's public receive URL, captured sample payload, and the ready-to-paste {{trigger.payload.*}} tokens for mapping payload values into automation steps (speak-upload name/sourceUrl, fieldsMap custom-field values, notify/outbound-webhook templates). Pass either the webhookId or the automationId of an inbound-webhook automation. If no sample has been captured yet, send a test payload to the inboundUrl first (append ?test=1 to capture without running the automation).",
|
|
4332
|
+
{
|
|
4333
|
+
webhookId: import_zod12.z.string().min(1).optional().describe("Inbound webhook id (from provision_inbound_webhook or an automation's trigger.webhookId)"),
|
|
4334
|
+
automationId: import_zod12.z.string().min(1).optional().describe("Automation id \u2014 resolves the bound webhookId and childKey automatically"),
|
|
4335
|
+
childKey: import_zod12.z.string().optional().describe("Override the dot-path used to narrow mappable payload paths (defaults to the automation's trigger.childKey)")
|
|
4336
|
+
},
|
|
4337
|
+
{
|
|
4338
|
+
title: "Get Inbound Webhook",
|
|
4339
|
+
readOnlyHint: true,
|
|
4340
|
+
destructiveHint: false,
|
|
4341
|
+
idempotentHint: true,
|
|
4342
|
+
openWorldHint: false
|
|
4343
|
+
},
|
|
4344
|
+
async ({ webhookId, automationId, childKey }) => {
|
|
4345
|
+
try {
|
|
4346
|
+
let resolvedWebhookId = webhookId;
|
|
4347
|
+
let resolvedChildKey = childKey;
|
|
4348
|
+
if (!resolvedWebhookId && automationId) {
|
|
4349
|
+
const resolved = await resolveAutomationInboundWebhook(api, automationId);
|
|
4350
|
+
if (!resolved.webhookId) {
|
|
4351
|
+
return {
|
|
4352
|
+
content: [
|
|
4353
|
+
{
|
|
4354
|
+
type: "text",
|
|
4355
|
+
text: `Error: automation ${automationId} has no inbound webhook bound to its trigger (it is not an inbound-webhook automation).`
|
|
4356
|
+
}
|
|
4357
|
+
],
|
|
4358
|
+
isError: true
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4361
|
+
resolvedWebhookId = resolved.webhookId;
|
|
4362
|
+
resolvedChildKey = resolvedChildKey ?? resolved.childKey;
|
|
4363
|
+
}
|
|
4364
|
+
if (!resolvedWebhookId) {
|
|
4365
|
+
return {
|
|
4366
|
+
content: [{ type: "text", text: "Error: provide either webhookId or automationId." }],
|
|
4367
|
+
isError: true
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
const info = await fetchInboundWebhookInfo(api, resolvedWebhookId, resolvedChildKey);
|
|
4371
|
+
return {
|
|
4372
|
+
content: [{ type: "text", text: JSON.stringify(info, null, 2) }]
|
|
4373
|
+
};
|
|
4374
|
+
} catch (err) {
|
|
4375
|
+
return {
|
|
4376
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
4377
|
+
isError: true
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
);
|
|
4382
|
+
registerSpeakTool(
|
|
4383
|
+
server,
|
|
4384
|
+
"get_webhook_attempts",
|
|
4385
|
+
"Get the delivery log for an inbound webhook: each received request with its HTTP acknowledgement status (200 = sample captured, 202 = accepted and run started, 401/403 = rejected) and the automation run it started. Use get_automation_runs for the run outcomes themselves.",
|
|
4386
|
+
{
|
|
4387
|
+
webhookId: import_zod12.z.string().min(1).describe("Unique identifier of the inbound webhook"),
|
|
4388
|
+
page: import_zod12.z.number().int().min(0).optional().describe("0-based page index"),
|
|
4389
|
+
pageSize: import_zod12.z.number().int().min(1).max(100).optional().describe("Results per page")
|
|
4390
|
+
},
|
|
4391
|
+
{
|
|
4392
|
+
title: "Get Webhook Attempts",
|
|
4393
|
+
readOnlyHint: true,
|
|
4394
|
+
destructiveHint: false,
|
|
4395
|
+
idempotentHint: true,
|
|
4396
|
+
openWorldHint: false
|
|
4397
|
+
},
|
|
4398
|
+
async ({ webhookId, ...params }) => {
|
|
4399
|
+
try {
|
|
4400
|
+
const result = await api.get(`/v1/webhook/${webhookId}/attempts`, { params });
|
|
4401
|
+
return {
|
|
4402
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
4403
|
+
};
|
|
4404
|
+
} catch (err) {
|
|
4405
|
+
return {
|
|
4406
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
4407
|
+
isError: true
|
|
4408
|
+
};
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
);
|
|
4187
4412
|
registerSpeakTool(
|
|
4188
4413
|
server,
|
|
4189
4414
|
"delete_webhook",
|
|
@@ -4220,6 +4445,7 @@ var init_webhooks = __esm({
|
|
|
4220
4445
|
import_zod12 = require("zod");
|
|
4221
4446
|
init_helpers();
|
|
4222
4447
|
init_client();
|
|
4448
|
+
init_inbound_webhook_utils();
|
|
4223
4449
|
}
|
|
4224
4450
|
});
|
|
4225
4451
|
|
|
@@ -4486,8 +4712,292 @@ var workflows_exports = {};
|
|
|
4486
4712
|
__export(workflows_exports, {
|
|
4487
4713
|
register: () => register14
|
|
4488
4714
|
});
|
|
4715
|
+
function tokenize(value) {
|
|
4716
|
+
if (typeof value !== "string") return value;
|
|
4717
|
+
if (value.includes("{{")) return value;
|
|
4718
|
+
if (value.startsWith("payload.")) return `{{trigger.payload.${value.slice("payload.".length)}}}`;
|
|
4719
|
+
return value;
|
|
4720
|
+
}
|
|
4721
|
+
async function loadFolders(api) {
|
|
4722
|
+
const res = await api.get("/v1/folder", { params: { pageSize: 500 } });
|
|
4723
|
+
const data = unwrapData(res.data) ?? {};
|
|
4724
|
+
const list = data.folderList ?? data.folders ?? (Array.isArray(data) ? data : []);
|
|
4725
|
+
return list.map((f) => ({
|
|
4726
|
+
folderId: String(f.folderId ?? f.id ?? ""),
|
|
4727
|
+
name: String(f.name ?? "")
|
|
4728
|
+
}));
|
|
4729
|
+
}
|
|
4730
|
+
async function loadFields(api) {
|
|
4731
|
+
const res = await api.get("/v1/fields");
|
|
4732
|
+
const data = unwrapData(res.data) ?? [];
|
|
4733
|
+
return data.map((f) => ({
|
|
4734
|
+
id: String(f.id ?? ""),
|
|
4735
|
+
name: String(f.name ?? "")
|
|
4736
|
+
}));
|
|
4737
|
+
}
|
|
4738
|
+
async function resolveFolder(api, ref, folders, createdFolders) {
|
|
4739
|
+
const byId = folders.find((f) => f.folderId === ref);
|
|
4740
|
+
if (byId) return byId.folderId;
|
|
4741
|
+
const byName = folders.find((f) => f.name.toLowerCase() === ref.toLowerCase());
|
|
4742
|
+
if (byName) return byName.folderId;
|
|
4743
|
+
if (ID_PATTERN.test(ref)) {
|
|
4744
|
+
throw new Error(`Folder id "${ref}" not found in this workspace (and it looks like an id, so it was not created as a folder name)`);
|
|
4745
|
+
}
|
|
4746
|
+
const res = await api.post("/v1/folder", { name: ref });
|
|
4747
|
+
const folderId = unwrapData(res.data)?.folderId;
|
|
4748
|
+
if (!folderId) throw new Error(`Could not create folder "${ref}"`);
|
|
4749
|
+
folders.push({ folderId, name: ref });
|
|
4750
|
+
createdFolders.push(`${ref} (${folderId})`);
|
|
4751
|
+
return folderId;
|
|
4752
|
+
}
|
|
4753
|
+
function resolveField(ref, fields) {
|
|
4754
|
+
const byId = fields.find((f) => f.id === ref);
|
|
4755
|
+
if (byId) return byId.id;
|
|
4756
|
+
const byName = fields.find((f) => f.name.toLowerCase() === ref.toLowerCase());
|
|
4757
|
+
if (byName) return byName.id;
|
|
4758
|
+
const available = fields.map((f) => f.name).slice(0, 25).join(", ");
|
|
4759
|
+
throw new Error(`Unknown custom field "${ref}". Available fields: ${available || "(none \u2014 create one with create_field)"}`);
|
|
4760
|
+
}
|
|
4489
4761
|
function register14(server, client) {
|
|
4490
4762
|
const api = client ?? speakClient;
|
|
4763
|
+
registerSpeakTool(
|
|
4764
|
+
server,
|
|
4765
|
+
"build_automation",
|
|
4766
|
+
"High-level automation builder: create (or update) a Speak automation from a friendly spec without knowing the wire format. Accepts folder/custom-field NAMES (resolved to ids; missing folders are auto-created), payload.<path> shorthand for webhook tokens, and simple step types (filter, branch, upload, ai_chat, translate, notify, call_webhook). For inbound-webhook automations the result includes the receive URL and mappable payload tokens. Prefer this over create_automation unless you need raw control.",
|
|
4767
|
+
buildAutomationSchema,
|
|
4768
|
+
{
|
|
4769
|
+
title: "Build Automation",
|
|
4770
|
+
readOnlyHint: false,
|
|
4771
|
+
destructiveHint: false,
|
|
4772
|
+
idempotentHint: false,
|
|
4773
|
+
openWorldHint: true
|
|
4774
|
+
},
|
|
4775
|
+
async (args2) => {
|
|
4776
|
+
const { name, trigger, steps, automationId, description, isActive, orTriggers } = args2;
|
|
4777
|
+
const createdFolders = [];
|
|
4778
|
+
const dataFlowFilterFields = [];
|
|
4779
|
+
try {
|
|
4780
|
+
let folders = null;
|
|
4781
|
+
let fields = null;
|
|
4782
|
+
const getFolders = async () => folders ?? (folders = await loadFolders(api));
|
|
4783
|
+
const getFields = async () => fields ?? (fields = await loadFields(api));
|
|
4784
|
+
const buildTrigger = async (spec, allowWebhook) => {
|
|
4785
|
+
const on = String(spec.on ?? "");
|
|
4786
|
+
if (on === "media_analyzed") {
|
|
4787
|
+
const refs = spec.folders ?? [];
|
|
4788
|
+
if (!refs.length) throw new Error("media_analyzed trigger requires `folders` (names or ids)");
|
|
4789
|
+
const folderIds = [];
|
|
4790
|
+
for (const ref of refs) folderIds.push(await resolveFolder(api, String(ref), await getFolders(), createdFolders));
|
|
4791
|
+
return { type: "folders", provider: "speak", app: "speak", triggerSlug: "media_analyzed", folderIds };
|
|
4792
|
+
}
|
|
4793
|
+
if (on === "inbound_webhook") {
|
|
4794
|
+
if (!allowWebhook) throw new Error("inbound_webhook cannot be used as an Or-trigger \u2014 make it the primary trigger");
|
|
4795
|
+
const t = { type: "folders", provider: "speak", app: "speak", triggerSlug: "inbound_webhook", folderIds: [] };
|
|
4796
|
+
if (spec.webhookId) t.webhookId = spec.webhookId;
|
|
4797
|
+
if (spec.childKey) t.childKey = spec.childKey;
|
|
4798
|
+
return t;
|
|
4799
|
+
}
|
|
4800
|
+
if (on === "field_updated") {
|
|
4801
|
+
const watch = spec.watchFields ?? [];
|
|
4802
|
+
if (!watch.length) throw new Error("field_updated trigger requires `watchFields`: [{ field, values? }]");
|
|
4803
|
+
const fieldList = await getFields();
|
|
4804
|
+
const values = [];
|
|
4805
|
+
const fieldValueMatches = [];
|
|
4806
|
+
for (const w of watch) {
|
|
4807
|
+
const fieldId = resolveField(String(w.field), fieldList);
|
|
4808
|
+
values.push(fieldId);
|
|
4809
|
+
if (Array.isArray(w.values) && w.values.length) {
|
|
4810
|
+
fieldValueMatches.push({ fieldId, values: w.values.map(String) });
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
const t = { type: "folders", provider: "speak", app: "speak", triggerSlug: "field_updated", folderIds: [], values };
|
|
4814
|
+
if (fieldValueMatches.length) t.fieldValueMatches = fieldValueMatches;
|
|
4815
|
+
if (spec.matchLogic === "AND") t.fieldMatchLogic = "AND";
|
|
4816
|
+
return t;
|
|
4817
|
+
}
|
|
4818
|
+
throw new Error(`Unknown trigger \`on\`: "${on}". Use media_analyzed, inbound_webhook, or field_updated.`);
|
|
4819
|
+
};
|
|
4820
|
+
const isWebhookAutomation = trigger.on === "inbound_webhook";
|
|
4821
|
+
const buildRules = async (rules, flowing2, isFilterStep) => {
|
|
4822
|
+
const out = [];
|
|
4823
|
+
for (const r of rules) {
|
|
4824
|
+
let field = String(r.field ?? "");
|
|
4825
|
+
if (flowing2 === "media" && !CANONICAL_FILTER_FIELDS.has(field) && !field.includes(".")) {
|
|
4826
|
+
field = resolveField(field, await getFields());
|
|
4827
|
+
} else if (isFilterStep && flowing2 === "data" && !CANONICAL_FILTER_FIELDS.has(field)) {
|
|
4828
|
+
dataFlowFilterFields.push(field);
|
|
4829
|
+
}
|
|
4830
|
+
const op = String(r.op ?? "eq");
|
|
4831
|
+
const rule = { field, op };
|
|
4832
|
+
if (r.value !== void 0) {
|
|
4833
|
+
rule.value = (op === "gt" || op === "lt") && Number.isFinite(Number(r.value)) ? Number(r.value) : r.value;
|
|
4834
|
+
}
|
|
4835
|
+
out.push(rule);
|
|
4836
|
+
}
|
|
4837
|
+
return out;
|
|
4838
|
+
};
|
|
4839
|
+
const wireSteps = [];
|
|
4840
|
+
let lastBranchStepId = null;
|
|
4841
|
+
let flowing = isWebhookAutomation ? "data" : "media";
|
|
4842
|
+
for (let i = 0; i < steps.length; i++) {
|
|
4843
|
+
const spec = steps[i];
|
|
4844
|
+
const stepId = `s${i + 1}`;
|
|
4845
|
+
const doType = String(spec.do ?? "");
|
|
4846
|
+
const step = { stepId };
|
|
4847
|
+
if (spec.runWhen === "true" || spec.runWhen === "false") {
|
|
4848
|
+
if (!lastBranchStepId) throw new Error(`Step ${i + 1}: runWhen requires an earlier branch step`);
|
|
4849
|
+
step.branch = spec.runWhen;
|
|
4850
|
+
step.dependsOn = [lastBranchStepId];
|
|
4851
|
+
}
|
|
4852
|
+
if (doType === "filter" || doType === "branch") {
|
|
4853
|
+
step.stepType = doType === "filter" ? "filter" : "condition";
|
|
4854
|
+
const block = {
|
|
4855
|
+
logic: spec.logic === "OR" ? "OR" : "AND",
|
|
4856
|
+
rules: await buildRules(spec.rules ?? [], flowing, doType === "filter")
|
|
4857
|
+
};
|
|
4858
|
+
step[doType === "filter" ? "filter" : "condition"] = block;
|
|
4859
|
+
if (doType === "branch") lastBranchStepId = stepId;
|
|
4860
|
+
} else if (doType === "upload") {
|
|
4861
|
+
if (!spec.source) throw new Error(`Step ${i + 1} (upload): \`source\` is required (URL or payload.<path>)`);
|
|
4862
|
+
const upload = {
|
|
4863
|
+
sourceMode: "url",
|
|
4864
|
+
sourceUrl: tokenize(spec.source),
|
|
4865
|
+
// Always defer until the uploaded media is PROCESSED so downstream
|
|
4866
|
+
// steps (ai_chat, translate) see the transcript — the web canvas
|
|
4867
|
+
// hardcodes this too.
|
|
4868
|
+
waitForProcessing: true
|
|
4869
|
+
};
|
|
4870
|
+
if (spec.name) upload.name = tokenize(spec.name);
|
|
4871
|
+
if (spec.language) upload.language = tokenize(spec.language);
|
|
4872
|
+
if (spec.folderFromPayload) {
|
|
4873
|
+
const rawKey = String(spec.folderFromPayload);
|
|
4874
|
+
const sourceKey = rawKey.includes("{{") ? rawKey : `{{trigger.payload.${rawKey.startsWith("payload.") ? rawKey.slice("payload.".length) : rawKey}}}`;
|
|
4875
|
+
upload.folderRouting = {
|
|
4876
|
+
mode: "dynamic",
|
|
4877
|
+
sourceKey,
|
|
4878
|
+
onNoMatch: spec.onNoFolderMatch === "default" ? "default" : "create"
|
|
4879
|
+
};
|
|
4880
|
+
if (spec.folder) upload.folderId = await resolveFolder(api, String(spec.folder), await getFolders(), createdFolders);
|
|
4881
|
+
} else {
|
|
4882
|
+
if (!spec.folder) throw new Error(`Step ${i + 1} (upload): provide \`folder\` (name or id) or \`folderFromPayload\``);
|
|
4883
|
+
upload.folderId = await resolveFolder(api, String(spec.folder), await getFolders(), createdFolders);
|
|
4884
|
+
}
|
|
4885
|
+
if (spec.mapFields && typeof spec.mapFields === "object") {
|
|
4886
|
+
const fieldList = await getFields();
|
|
4887
|
+
const fieldsMap = {};
|
|
4888
|
+
for (const [ref, value] of Object.entries(spec.mapFields)) {
|
|
4889
|
+
if (value !== null && typeof value === "object") {
|
|
4890
|
+
throw new Error(`Step ${i + 1} (upload): mapFields["${ref}"] must be a string or number, not an object`);
|
|
4891
|
+
}
|
|
4892
|
+
fieldsMap[resolveField(ref, fieldList)] = tokenize(String(value));
|
|
4893
|
+
}
|
|
4894
|
+
if (Object.keys(fieldsMap).length) upload.fieldsMap = fieldsMap;
|
|
4895
|
+
}
|
|
4896
|
+
step.stepType = "speak-upload";
|
|
4897
|
+
step.speakUpload = upload;
|
|
4898
|
+
flowing = "media";
|
|
4899
|
+
} else if (doType === "ai_chat") {
|
|
4900
|
+
const hasSaveToFields = Array.isArray(spec.saveToFields) && spec.saveToFields.length > 0;
|
|
4901
|
+
if (!spec.prompt && !hasSaveToFields) {
|
|
4902
|
+
throw new Error(`Step ${i + 1} (ai_chat): provide \`prompt\`, \`saveToFields\`, or both`);
|
|
4903
|
+
}
|
|
4904
|
+
const magicPrompt = { prompt: spec.prompt ?? "", assistantType: "general" };
|
|
4905
|
+
if (spec.title) magicPrompt.title = spec.title;
|
|
4906
|
+
if (spec.model) magicPrompt.modelId = spec.model;
|
|
4907
|
+
if (Array.isArray(spec.saveToFields) && spec.saveToFields.length) {
|
|
4908
|
+
if (spec.saveToFields.length > 10) {
|
|
4909
|
+
throw new Error(`Step ${i + 1} (ai_chat): saveToFields supports at most 10 fields`);
|
|
4910
|
+
}
|
|
4911
|
+
const fieldList = await getFields();
|
|
4912
|
+
magicPrompt.fieldIds = spec.saveToFields.map((ref) => resolveField(String(ref), fieldList));
|
|
4913
|
+
}
|
|
4914
|
+
step.stepType = "magic-prompt";
|
|
4915
|
+
step.magicPrompt = magicPrompt;
|
|
4916
|
+
flowing = "insight";
|
|
4917
|
+
} else if (doType === "translate") {
|
|
4918
|
+
if (!spec.language) throw new Error(`Step ${i + 1} (translate): \`language\` is required (e.g. "es-ES")`);
|
|
4919
|
+
step.stepType = "translation";
|
|
4920
|
+
step.translation = { targetLanguage: spec.language };
|
|
4921
|
+
flowing = "media";
|
|
4922
|
+
} else if (doType === "notify") {
|
|
4923
|
+
if (!spec.message) throw new Error(`Step ${i + 1} (notify): \`message\` is required`);
|
|
4924
|
+
const channel = spec.channel === void 0 ? "in_app" : String(spec.channel);
|
|
4925
|
+
if (!["in_app", "email", "slack"].includes(channel)) {
|
|
4926
|
+
throw new Error(`Step ${i + 1} (notify): channel must be "in_app", "email", or "slack" (got "${channel}")`);
|
|
4927
|
+
}
|
|
4928
|
+
const notify = { channel, message: tokenize(spec.message) };
|
|
4929
|
+
if (spec.target) notify.target = String(spec.target);
|
|
4930
|
+
step.stepType = "notify";
|
|
4931
|
+
step.notify = notify;
|
|
4932
|
+
flowing = "data";
|
|
4933
|
+
} else if (doType === "call_webhook") {
|
|
4934
|
+
if (!spec.url) throw new Error(`Step ${i + 1} (call_webhook): \`url\` is required`);
|
|
4935
|
+
const method = spec.method === void 0 ? "POST" : String(spec.method).toUpperCase();
|
|
4936
|
+
if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
|
4937
|
+
throw new Error(`Step ${i + 1} (call_webhook): method must be GET, POST, PUT, PATCH, or DELETE (got "${spec.method}")`);
|
|
4938
|
+
}
|
|
4939
|
+
const outbound = {
|
|
4940
|
+
url: tokenize(spec.url),
|
|
4941
|
+
method
|
|
4942
|
+
};
|
|
4943
|
+
if (spec.headers && typeof spec.headers === "object") outbound.headers = spec.headers;
|
|
4944
|
+
if (spec.body !== void 0) {
|
|
4945
|
+
outbound.bodyTemplate = typeof spec.body === "string" ? tokenize(spec.body) : spec.body;
|
|
4946
|
+
}
|
|
4947
|
+
step.stepType = "outbound-webhook";
|
|
4948
|
+
step.outboundWebhook = outbound;
|
|
4949
|
+
flowing = "data";
|
|
4950
|
+
} else {
|
|
4951
|
+
throw new Error(
|
|
4952
|
+
`Step ${i + 1}: unknown \`do\`: "${doType}". Use filter, branch, upload, ai_chat, translate, notify, or call_webhook.`
|
|
4953
|
+
);
|
|
4954
|
+
}
|
|
4955
|
+
wireSteps.push(step);
|
|
4956
|
+
}
|
|
4957
|
+
const body = {
|
|
4958
|
+
name,
|
|
4959
|
+
trigger: await buildTrigger(trigger, true),
|
|
4960
|
+
steps: wireSteps
|
|
4961
|
+
};
|
|
4962
|
+
if (description) body.description = description;
|
|
4963
|
+
if (isActive !== void 0) body.isActive = isActive;
|
|
4964
|
+
if (orTriggers?.length) {
|
|
4965
|
+
const entries = [];
|
|
4966
|
+
for (const spec of orTriggers) entries.push(await buildTrigger(spec, false));
|
|
4967
|
+
body.triggers = entries;
|
|
4968
|
+
}
|
|
4969
|
+
const result = automationId ? await api.put(`/v1/automations/${automationId}`, body) : await api.post("/v1/automations/", body);
|
|
4970
|
+
const resolvedId = unwrapData(result.data)?.automationId ?? automationId;
|
|
4971
|
+
const response = {
|
|
4972
|
+
...typeof result.data === "object" ? result.data : { data: result.data }
|
|
4973
|
+
};
|
|
4974
|
+
if (createdFolders.length) response.createdFolders = createdFolders;
|
|
4975
|
+
if (isWebhookAutomation && resolvedId) {
|
|
4976
|
+
try {
|
|
4977
|
+
const resolved = await resolveAutomationInboundWebhook(api, resolvedId);
|
|
4978
|
+
if (resolved.webhookId) {
|
|
4979
|
+
response.inboundWebhook = await fetchInboundWebhookInfo(api, resolved.webhookId, resolved.childKey);
|
|
4980
|
+
}
|
|
4981
|
+
} catch {
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
return {
|
|
4985
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
4986
|
+
};
|
|
4987
|
+
} catch (err) {
|
|
4988
|
+
let message = formatAxiosError(err);
|
|
4989
|
+
if (dataFlowFilterFields.length && message.includes("fieldIds do not belong")) {
|
|
4990
|
+
message += `
|
|
4991
|
+
|
|
4992
|
+
Likely cause: this server rejects filter rules on webhook payload fields (${dataFlowFilterFields.join(", ")}) at publish time (known server-side validation gap). Workarounds: move the filter AFTER the upload step and filter on media/custom fields instead, or filter in the sending system before it posts to the webhook.`;
|
|
4993
|
+
}
|
|
4994
|
+
return {
|
|
4995
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
4996
|
+
isError: true
|
|
4997
|
+
};
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
);
|
|
4491
5001
|
registerSpeakTool(
|
|
4492
5002
|
server,
|
|
4493
5003
|
"upload_and_analyze",
|
|
@@ -4642,7 +5152,7 @@ ${JSON.stringify(signedRes.data, null, 2)}` }],
|
|
|
4642
5152
|
}
|
|
4643
5153
|
);
|
|
4644
5154
|
}
|
|
4645
|
-
var import_zod15, fs, path2;
|
|
5155
|
+
var import_zod15, fs, path2, CANONICAL_FILTER_FIELDS, ID_PATTERN, TRIGGER_SPEC_DESCRIPTION, STEP_SPEC_DESCRIPTION, buildAutomationSchema;
|
|
4646
5156
|
var init_workflows = __esm({
|
|
4647
5157
|
"src/tools/workflows.ts"() {
|
|
4648
5158
|
"use strict";
|
|
@@ -4653,6 +5163,34 @@ var init_workflows = __esm({
|
|
|
4653
5163
|
fs = __toESM(require("fs"));
|
|
4654
5164
|
path2 = __toESM(require("path"));
|
|
4655
5165
|
init_media_utils();
|
|
5166
|
+
init_inbound_webhook_utils();
|
|
5167
|
+
CANONICAL_FILTER_FIELDS = /* @__PURE__ */ new Set([
|
|
5168
|
+
"name",
|
|
5169
|
+
"duration",
|
|
5170
|
+
"sourceLanguage",
|
|
5171
|
+
"tags",
|
|
5172
|
+
"transcript",
|
|
5173
|
+
"speakers",
|
|
5174
|
+
"answer",
|
|
5175
|
+
// server-side aliases
|
|
5176
|
+
"title",
|
|
5177
|
+
"language",
|
|
5178
|
+
"speakersCount"
|
|
5179
|
+
]);
|
|
5180
|
+
ID_PATTERN = /^[0-9a-f]{12}$/;
|
|
5181
|
+
TRIGGER_SPEC_DESCRIPTION = 'What starts the automation. Object with:\n- on (required): "media_analyzed" | "inbound_webhook" | "field_updated"\n- folders: array of folder names or ids (required for media_analyzed; missing folders are created)\n- childKey: dot-path narrowing the webhook payload root, e.g. "data" (inbound_webhook only)\n- webhookId: reuse a webhook from provision_inbound_webhook (inbound_webhook only; omit to auto-provision)\n- watchFields: array of { field: name-or-id, values?: string[] } (required for field_updated \u2014 fires when the field changes; values restricts to specific new values)\n- matchLogic: "AND"|"OR" for combining multiple watchFields value matches (default OR)';
|
|
5182
|
+
STEP_SPEC_DESCRIPTION = 'Ordered actions. Each step is an object with a `do` key plus its options. String values may be literals, "payload.<path>" shorthand (converted to {{trigger.payload.<path>}} only when it is the ENTIRE value), or raw {{...}} tokens \u2014 inside longer text, write the full {{trigger.payload.<path>}} form.\n- { do: "filter", rules: [{ field, op, value? }], logic?: "AND"|"OR" } \u2014 continue only if rules match. Fields: media flows use name|duration|sourceLanguage|tags|transcript|speakers or a custom field name; webhook payloads use payload paths like "contact.status". Ops: eq|neq|contains|ncontains|startsWith|gt|lt|exists\n- { do: "branch", rules, logic? } \u2014 like filter but routes instead of stopping; later steps with runWhen: "true"|"false" only run on that outcome. NOTE: branch routing requires the server\'s DAG runner (feature-flagged); when it is off, steps run in order and runWhen markers are ignored \u2014 prefer filter for guaranteed gating\n- { do: "upload", source (URL or payload.<path>, required), name?, language? (e.g. "en-US"), folder? (name or id; created if missing), folderFromPayload? (payload key holding the destination folder name \u2014 dynamic routing), onNoFolderMatch?: "create"|"default", mapFields?: { <field name or id>: <value or payload.<path>> } (writes payload values into custom fields on the uploaded media) }\n- { do: "ai_chat", prompt? (required unless saveToFields given), title?, saveToFields?: [field names or ids] (max 10 \u2014 values are extracted into these custom fields; prompt may be omitted for extraction-only steps), model? (a Speak-supported LLM id, e.g. "gemini-2.5-flash", "claude-sonnet-4-6"; omit for the workspace default) }\n- { do: "translate", language: region-qualified code like "es-ES", "fr-FR" }\n- { do: "notify", message (required, tokens allowed), channel?: "in_app"|"email"|"slack" (default in_app; email currently falls back to an in-app notification), target? (reserved \u2014 not yet used for delivery) }\n- { do: "call_webhook", url (required), method?, headers?, body? (string or object template, tokens allowed) }\nSteps may also set runWhen (after a branch step). Composio app actions (Google Drive, Slack apps, \u2026) are not supported by this builder yet \u2014 use create_automation directly for those.';
|
|
5183
|
+
buildAutomationSchema = {
|
|
5184
|
+
name: import_zod15.z.string().min(1).max(150).describe("Display name for the automation"),
|
|
5185
|
+
trigger: import_zod15.z.record(import_zod15.z.unknown()).describe(TRIGGER_SPEC_DESCRIPTION),
|
|
5186
|
+
steps: import_zod15.z.array(import_zod15.z.record(import_zod15.z.unknown())).min(1).max(20).describe(STEP_SPEC_DESCRIPTION),
|
|
5187
|
+
automationId: import_zod15.z.string().optional().describe("Update this existing automation instead of creating a new one (full replace)"),
|
|
5188
|
+
description: import_zod15.z.string().max(1e3).optional().describe("Optional description"),
|
|
5189
|
+
isActive: import_zod15.z.boolean().optional().describe("Whether the automation is active (default true)"),
|
|
5190
|
+
orTriggers: import_zod15.z.array(import_zod15.z.record(import_zod15.z.unknown())).max(10).optional().describe(
|
|
5191
|
+
'Additional "Or" triggers (same shape as trigger, but inbound_webhook is not allowed here). The automation runs when ANY trigger fires.'
|
|
5192
|
+
)
|
|
5193
|
+
};
|
|
4656
5194
|
}
|
|
4657
5195
|
});
|
|
4658
5196
|
|
|
@@ -5715,8 +6253,12 @@ var init_tool_names = __esm({
|
|
|
5715
6253
|
"list_webhooks",
|
|
5716
6254
|
"create_webhook",
|
|
5717
6255
|
"update_webhook",
|
|
6256
|
+
"provision_inbound_webhook",
|
|
6257
|
+
"get_inbound_webhook",
|
|
6258
|
+
"get_webhook_attempts",
|
|
5718
6259
|
"delete_webhook",
|
|
5719
|
-
// workflows (high-level wrappers around media + upload tools)
|
|
6260
|
+
// workflows (high-level wrappers around media + upload + automation tools)
|
|
6261
|
+
"build_automation",
|
|
5720
6262
|
"upload_and_analyze",
|
|
5721
6263
|
"upload_local_file",
|
|
5722
6264
|
// users / team management
|
|
@@ -6905,6 +7447,11 @@ var cliCommands = [
|
|
|
6905
7447
|
"clips",
|
|
6906
7448
|
"clip",
|
|
6907
7449
|
"schedule-meeting",
|
|
7450
|
+
"list-meeting-events",
|
|
7451
|
+
"live-transcript",
|
|
7452
|
+
"move",
|
|
7453
|
+
"tools",
|
|
7454
|
+
"call",
|
|
6908
7455
|
"help"
|
|
6909
7456
|
];
|
|
6910
7457
|
var isCliMode = args.length > 0 && (args[0].startsWith("-") || cliCommands.includes(args[0]));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speakai/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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",
|