@speakai/mcp-server 1.13.7 → 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 +561 -23
- 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)"),
|
|
@@ -2944,22 +2955,14 @@ function register7(server, client) {
|
|
|
2944
2955
|
);
|
|
2945
2956
|
registerSpeakTool(
|
|
2946
2957
|
server,
|
|
2947
|
-
"
|
|
2948
|
-
`[Deprecated \u2014 use ask_ai_chat] ${askAiChatDescription}`,
|
|
2949
|
-
askAiChatInputSchema,
|
|
2950
|
-
askAiChatAnnotations,
|
|
2951
|
-
askAiChatHandler
|
|
2952
|
-
);
|
|
2953
|
-
registerSpeakTool(
|
|
2954
|
-
server,
|
|
2955
|
-
"retry_magic_prompt",
|
|
2958
|
+
"retry_ai_chat",
|
|
2956
2959
|
"Retry a failed or incomplete AI Chat response. Use when a previous ask_ai_chat call returned an error or incomplete answer.",
|
|
2957
2960
|
{
|
|
2958
2961
|
promptId: import_zod8.z.string().min(1).describe("ID of the conversation containing the failed message"),
|
|
2959
2962
|
messageId: import_zod8.z.string().min(1).describe("ID of the specific message to retry")
|
|
2960
2963
|
},
|
|
2961
2964
|
{
|
|
2962
|
-
title: "Retry AI
|
|
2965
|
+
title: "Retry AI Chat",
|
|
2963
2966
|
readOnlyHint: false,
|
|
2964
2967
|
destructiveHint: false,
|
|
2965
2968
|
idempotentHint: false,
|
|
@@ -3668,11 +3671,87 @@ var init_fields2 = __esm({
|
|
|
3668
3671
|
}
|
|
3669
3672
|
});
|
|
3670
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
|
+
|
|
3671
3738
|
// src/tools/automations.ts
|
|
3672
3739
|
var automations_exports = {};
|
|
3673
3740
|
__export(automations_exports, {
|
|
3674
3741
|
register: () => register10
|
|
3675
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
|
+
}
|
|
3676
3755
|
function register10(server, client) {
|
|
3677
3756
|
const api = client ?? speakClient;
|
|
3678
3757
|
registerSpeakTool(
|
|
@@ -3797,7 +3876,7 @@ function register10(server, client) {
|
|
|
3797
3876
|
registerSpeakTool(
|
|
3798
3877
|
server,
|
|
3799
3878
|
"create_automation",
|
|
3800
|
-
"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.",
|
|
3801
3880
|
writeSchema,
|
|
3802
3881
|
{
|
|
3803
3882
|
title: "Create Automation",
|
|
@@ -3809,8 +3888,13 @@ function register10(server, client) {
|
|
|
3809
3888
|
async (body) => {
|
|
3810
3889
|
try {
|
|
3811
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
|
+
}
|
|
3812
3896
|
return {
|
|
3813
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
3897
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
3814
3898
|
};
|
|
3815
3899
|
} catch (err) {
|
|
3816
3900
|
return {
|
|
@@ -3838,8 +3922,12 @@ function register10(server, client) {
|
|
|
3838
3922
|
async ({ automationId, ...body }) => {
|
|
3839
3923
|
try {
|
|
3840
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
|
+
}
|
|
3841
3929
|
return {
|
|
3842
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
3930
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
3843
3931
|
};
|
|
3844
3932
|
} catch (err) {
|
|
3845
3933
|
return {
|
|
@@ -4075,18 +4163,27 @@ function register10(server, client) {
|
|
|
4075
4163
|
}
|
|
4076
4164
|
);
|
|
4077
4165
|
}
|
|
4078
|
-
var import_zod11, STEPS_DESCRIPTION, TRIGGER_DESCRIPTION, writeSchema;
|
|
4166
|
+
var import_zod11, TOKEN_SYNTAX_NOTE, STEPS_DESCRIPTION, TRIGGER_DESCRIPTION, OR_TRIGGERS_DESCRIPTION, writeSchema;
|
|
4079
4167
|
var init_automations = __esm({
|
|
4080
4168
|
"src/tools/automations.ts"() {
|
|
4081
4169
|
"use strict";
|
|
4082
4170
|
import_zod11 = require("zod");
|
|
4083
4171
|
init_helpers();
|
|
4084
4172
|
init_client();
|
|
4085
|
-
|
|
4086
|
-
|
|
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>"] }]';
|
|
4087
4183
|
writeSchema = {
|
|
4088
4184
|
name: import_zod11.z.string().min(1).max(150).describe("Display name for the automation"),
|
|
4089
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),
|
|
4090
4187
|
steps: import_zod11.z.array(import_zod11.z.record(import_zod11.z.unknown())).min(1).max(20).describe(STEPS_DESCRIPTION),
|
|
4091
4188
|
description: import_zod11.z.string().max(1e3).optional().describe("Optional description"),
|
|
4092
4189
|
isActive: import_zod11.z.boolean().optional().describe("Whether the automation is active (defaults to true)"),
|
|
@@ -4192,6 +4289,126 @@ function register11(server, client) {
|
|
|
4192
4289
|
}
|
|
4193
4290
|
}
|
|
4194
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
|
+
);
|
|
4195
4412
|
registerSpeakTool(
|
|
4196
4413
|
server,
|
|
4197
4414
|
"delete_webhook",
|
|
@@ -4228,6 +4445,7 @@ var init_webhooks = __esm({
|
|
|
4228
4445
|
import_zod12 = require("zod");
|
|
4229
4446
|
init_helpers();
|
|
4230
4447
|
init_client();
|
|
4448
|
+
init_inbound_webhook_utils();
|
|
4231
4449
|
}
|
|
4232
4450
|
});
|
|
4233
4451
|
|
|
@@ -4494,8 +4712,292 @@ var workflows_exports = {};
|
|
|
4494
4712
|
__export(workflows_exports, {
|
|
4495
4713
|
register: () => register14
|
|
4496
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
|
+
}
|
|
4497
4761
|
function register14(server, client) {
|
|
4498
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
|
+
);
|
|
4499
5001
|
registerSpeakTool(
|
|
4500
5002
|
server,
|
|
4501
5003
|
"upload_and_analyze",
|
|
@@ -4650,7 +5152,7 @@ ${JSON.stringify(signedRes.data, null, 2)}` }],
|
|
|
4650
5152
|
}
|
|
4651
5153
|
);
|
|
4652
5154
|
}
|
|
4653
|
-
var import_zod15, fs, path2;
|
|
5155
|
+
var import_zod15, fs, path2, CANONICAL_FILTER_FIELDS, ID_PATTERN, TRIGGER_SPEC_DESCRIPTION, STEP_SPEC_DESCRIPTION, buildAutomationSchema;
|
|
4654
5156
|
var init_workflows = __esm({
|
|
4655
5157
|
"src/tools/workflows.ts"() {
|
|
4656
5158
|
"use strict";
|
|
@@ -4661,6 +5163,34 @@ var init_workflows = __esm({
|
|
|
4661
5163
|
fs = __toESM(require("fs"));
|
|
4662
5164
|
path2 = __toESM(require("path"));
|
|
4663
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
|
+
};
|
|
4664
5194
|
}
|
|
4665
5195
|
});
|
|
4666
5196
|
|
|
@@ -5692,7 +6222,6 @@ var init_tool_names = __esm({
|
|
|
5692
6222
|
"get_live_meeting_transcript",
|
|
5693
6223
|
// prompt
|
|
5694
6224
|
"ask_ai_chat",
|
|
5695
|
-
"ask_magic_prompt",
|
|
5696
6225
|
"list_prompts",
|
|
5697
6226
|
"get_favorite_prompts",
|
|
5698
6227
|
"toggle_prompt_favorite",
|
|
@@ -5701,7 +6230,7 @@ var init_tool_names = __esm({
|
|
|
5701
6230
|
"update_chat_title",
|
|
5702
6231
|
"delete_chat_message",
|
|
5703
6232
|
"submit_chat_feedback",
|
|
5704
|
-
"
|
|
6233
|
+
"retry_ai_chat",
|
|
5705
6234
|
"export_chat_answer",
|
|
5706
6235
|
"get_chat_statistics",
|
|
5707
6236
|
// recorder
|
|
@@ -5724,8 +6253,12 @@ var init_tool_names = __esm({
|
|
|
5724
6253
|
"list_webhooks",
|
|
5725
6254
|
"create_webhook",
|
|
5726
6255
|
"update_webhook",
|
|
6256
|
+
"provision_inbound_webhook",
|
|
6257
|
+
"get_inbound_webhook",
|
|
6258
|
+
"get_webhook_attempts",
|
|
5727
6259
|
"delete_webhook",
|
|
5728
|
-
// workflows (high-level wrappers around media + upload tools)
|
|
6260
|
+
// workflows (high-level wrappers around media + upload + automation tools)
|
|
6261
|
+
"build_automation",
|
|
5729
6262
|
"upload_and_analyze",
|
|
5730
6263
|
"upload_local_file",
|
|
5731
6264
|
// users / team management
|
|
@@ -6914,6 +7447,11 @@ var cliCommands = [
|
|
|
6914
7447
|
"clips",
|
|
6915
7448
|
"clip",
|
|
6916
7449
|
"schedule-meeting",
|
|
7450
|
+
"list-meeting-events",
|
|
7451
|
+
"live-transcript",
|
|
7452
|
+
"move",
|
|
7453
|
+
"tools",
|
|
7454
|
+
"call",
|
|
6917
7455
|
"help"
|
|
6918
7456
|
];
|
|
6919
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",
|