@trillet-ai/mcp 0.7.3

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.
@@ -0,0 +1,337 @@
1
+ import { z } from "zod";
2
+ import { ok, guard, workspaceParam } from "../helpers.js";
3
+ /** In-call API tool ("webhook") definition, mirrors the Pathway schema. */
4
+ const incallApiToolSchema = z.object({
5
+ name: z.string().describe('Tool name the LLM will call, e.g. "lookup_customer"'),
6
+ description: z
7
+ .string()
8
+ .describe("Tells the LLM what the tool does and when to call it. Be specific."),
9
+ url: z
10
+ .string()
11
+ .describe("Request URL. Supports placeholders: {{_variable.<name>}} (LLM-passed args), {{_dynamic_variable.<key>}}, {{_secret.<name>}}, {{_metadata.<key>}}."),
12
+ method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).default("POST"),
13
+ headers: z
14
+ .array(z.object({ name: z.string(), value: z.string() }))
15
+ .default([])
16
+ .describe("Header list as name/value pairs (NOT an object). Values support placeholders."),
17
+ body: z
18
+ .string()
19
+ .optional()
20
+ .describe("JSON string template. For GET/DELETE the parsed body is flattened into query parameters. Placeholders resolve per-value, so exact-match placeholders keep their native JSON type."),
21
+ bodyType: z.enum(["json", "text"]).default("json"),
22
+ variables: z
23
+ .array(z.object({ name: z.string(), description: z.string() }))
24
+ .default([])
25
+ .describe("Parameters the LLM may pass (name + description only; no types). These become {{_variable.<name>}} in url/headers/body. The LLM is told these are the ONLY valid keys."),
26
+ responseVariables: z
27
+ .array(z.object({ key: z.string(), path: z.string() }))
28
+ .default([])
29
+ .describe('Extract values from the JSON response via dot-path (e.g. "records.0.fields.Name") into {{_response.<key>}} for later tools. Also lands in post-call analyzedData.'),
30
+ textToSpeak: z
31
+ .string()
32
+ .optional()
33
+ .describe('Optional phrase spoken while the request runs, e.g. "One moment while I check that."'),
34
+ timeout: z.number().default(15).describe("Request timeout in seconds (platform default 20)"),
35
+ status: z.boolean().default(true).describe("false disables the tool without deleting it"),
36
+ });
37
+ const postCallFieldSchema = z.object({
38
+ name: z.string().describe("snake_case field name; becomes a key in analyzedData"),
39
+ type: z.enum(["text", "boolean", "number", "selector"]).default("text"),
40
+ description: z
41
+ .string()
42
+ .describe("Extraction instruction for the analysis model. Be explicit about when to return null."),
43
+ options: z
44
+ .array(z.string())
45
+ .optional()
46
+ .describe("For type=selector: the exact allowed values (becomes an enum)"),
47
+ });
48
+ function summarizeFlow(p) {
49
+ const s = p.settings ?? {};
50
+ const wm = s.welcomeMessageConfig ?? {};
51
+ return {
52
+ id: p._id,
53
+ name: p.name,
54
+ direction: p.direction,
55
+ promptType: p.promptType,
56
+ agentId: typeof p.agent === "object" ? p.agent?._id : p.agent,
57
+ promptLength: (p.prompt || "").length,
58
+ inCallApiTools: (p.webhooks ?? []).map((w) => w.name),
59
+ knowledgeBases: s.knowledgeBases ?? [],
60
+ postCallAnalysisFields: (s.postCallAnalysis ?? []).map((f) => f.name),
61
+ // Settings the assistant most often writes, echoed so a read-back after an
62
+ // update shows the value instead of looking like the write was lost.
63
+ settings: {
64
+ callSetting: s.callSetting ?? {},
65
+ customerLocation: s.customerLocation ?? null,
66
+ timeContextEnabled: s.timeContextEnabled ?? null,
67
+ enableRecording: s.enableRecording ?? null,
68
+ security: s.security ?? {},
69
+ backgroundNoise: s.backgroundNoise ?? null,
70
+ officeHoursEnabled: s.OfficeHoursSettings?.enabled ?? null,
71
+ welcomeMessage: {
72
+ inbound: wm.Inbound?.selected ?? null,
73
+ outbound: wm.Outbound?.selected ?? null,
74
+ },
75
+ transfers: (s.transfers ?? []).map((t) => `${t.friendly_name} (${t.method ?? "cold"} -> ${t.destination})`),
76
+ enableHumanLikeVoiceAndTone: s.enableHumanLikeVoiceAndTone ?? null,
77
+ enableTransferInstructions: s.enableTransferInstructions ?? null,
78
+ enableEndCallInstructions: s.enableEndCallInstructions ?? null,
79
+ },
80
+ hasActiveDraft: p._hasDraft ?? undefined,
81
+ updatedAt: p.updatedAt,
82
+ };
83
+ }
84
+ export function registerCallFlowTools(server, client) {
85
+ server.registerTool("trillet_list_call_flows", {
86
+ title: "List call flows",
87
+ description: "List all call flows (pathways) in the workspace with compact summaries.",
88
+ inputSchema: { ...workspaceParam },
89
+ annotations: { readOnlyHint: true, openWorldHint: true },
90
+ }, guard(async ({ workspace_id }) => {
91
+ const res = await client.request("GET", "/v1/api/call-flows", {
92
+ workspaceId: workspace_id,
93
+ });
94
+ const list = Array.isArray(res) ? res : res.pathways ?? [];
95
+ return ok(list.map(summarizeFlow));
96
+ }));
97
+ server.registerTool("trillet_get_call_flow", {
98
+ title: "Get call flow",
99
+ description: "Fetch one call flow. IMPORTANT: if the flow has an active editor draft, this endpoint returns the DRAFT merged over production, and test calls also run the draft. The response includes a has_active_draft flag; use trillet_deactivate_drafts to return to production.",
100
+ inputSchema: {
101
+ flow_id: z.string(),
102
+ include_prompt: z
103
+ .boolean()
104
+ .default(false)
105
+ .describe("true returns the full prompt text (can be long)"),
106
+ ...workspaceParam,
107
+ },
108
+ annotations: { readOnlyHint: true, openWorldHint: true },
109
+ }, guard(async ({ flow_id, include_prompt, workspace_id }) => {
110
+ const res = await client.request("GET", `/v1/api/call-flows/${flow_id}`, {
111
+ workspaceId: workspace_id,
112
+ });
113
+ const p = res.pathway ?? res;
114
+ const drafts = await client
115
+ .request("GET", `/v1/api/call-flows/${flow_id}/drafts`, { workspaceId: workspace_id })
116
+ .catch(() => []);
117
+ const draftList = Array.isArray(drafts) ? drafts : drafts.drafts ?? [];
118
+ const summary = summarizeFlow(p);
119
+ const out = {
120
+ ...summary,
121
+ has_active_draft: draftList.some((d) => d.isActive),
122
+ // Keep the summary's settings echo (the assistant reads this back after
123
+ // every write) and add the full shapes this endpoint is used for.
124
+ settings: {
125
+ ...summary.settings,
126
+ welcomeMessageConfig: p.settings?.welcomeMessageConfig,
127
+ transfersFull: p.settings?.transfers,
128
+ postCallAnalysis: p.settings?.postCallAnalysis,
129
+ },
130
+ inCallApiToolsFull: p.webhooks,
131
+ dynamicVariableCollections: p.dynamicVariableCollections,
132
+ };
133
+ if (include_prompt)
134
+ out.prompt = p.prompt;
135
+ return ok(out);
136
+ }));
137
+ server.registerTool("trillet_create_call_flow", {
138
+ title: "Create call flow",
139
+ description: "Create a new call flow. GOTCHA: on CREATE, a partial `settings` object replaces ALL defaults, so this tool intentionally does not accept settings; create first, then configure with the update tools (updates deep-merge safely).",
140
+ inputSchema: {
141
+ name: z.string(),
142
+ direction: z
143
+ .enum(["outbound", "bidirectional"])
144
+ .default("bidirectional")
145
+ .describe("bidirectional answers inbound calls (there is no separate inbound mode: the platform stores inbound as bidirectional); outbound is dial-out only"),
146
+ prompt: z.string().optional(),
147
+ agent_id: z.string().optional().describe("Link to an existing agent"),
148
+ ...workspaceParam,
149
+ },
150
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
151
+ }, guard(async ({ name, direction, prompt, agent_id, workspace_id }) => {
152
+ const body = { name, direction, promptType: "simple" };
153
+ if (prompt)
154
+ body.prompt = prompt;
155
+ if (agent_id)
156
+ body.agent = agent_id;
157
+ const res = await client.request("POST", "/v1/api/call-flows", {
158
+ body,
159
+ workspaceId: workspace_id,
160
+ });
161
+ return ok(res);
162
+ }));
163
+ server.registerTool("trillet_update_call_flow", {
164
+ title: "Update call flow (advanced)",
165
+ description: "Partial update of any call-flow fields. Merge semantics (verified): objects DEEP-MERGE (sending settings.callSetting.x preserves siblings), but ARRAYS REPLACE WHOLESALE (webhooks, transfers, postCallAnalysis must be sent complete), and null values are ignored (you cannot null a field). Prefer the dedicated tools for prompt, in-call APIs, and post-call analysis. Top-level settings toggles reachable ONLY through this tool: settings.enableHumanLikeVoiceAndTone (expressive delivery), settings.enableTransferInstructions and settings.enableEndCallInstructions (whether the transfer / end-call prompt blocks are injected; both default on), settings.customerLocation (IANA timezone) and settings.timeContextEnabled (agent knows the current date/time; default on).",
166
+ inputSchema: {
167
+ flow_id: z.string(),
168
+ update: z
169
+ .record(z.string(), z.unknown())
170
+ .describe('Fields to update, e.g. {"name": "...", "settings": {"callSetting": {"maxCallDuration": 900}}}'),
171
+ ...workspaceParam,
172
+ },
173
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
174
+ }, guard(async ({ flow_id, update, workspace_id }) => {
175
+ const res = await client.request("PUT", `/v1/api/call-flows/${flow_id}`, {
176
+ body: update,
177
+ workspaceId: workspace_id,
178
+ });
179
+ return ok(summarizeFlow(res.pathway ?? res));
180
+ }));
181
+ server.registerTool("trillet_update_prompt", {
182
+ title: "Update prompt",
183
+ description: "Replace a call flow's prompt. The API auto-snapshots a prompt version on every update. " +
184
+ "Prompt conventions: reference in-call API tools as {{realtime_api \"tool_name\" \"var1, var2\"}} (the canonical token the runtime documents to the model); " +
185
+ "{{key}} placeholders are substituted from per-call dynamic variables; do NOT hardcode a scripted greeting in the prompt because the platform welcome message already plays it (a scripted 'Begin with' line causes the agent to greet twice).",
186
+ inputSchema: { flow_id: z.string(), prompt: z.string(), ...workspaceParam },
187
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
188
+ }, guard(async ({ flow_id, prompt, workspace_id }) => {
189
+ const res = await client.request("PUT", `/v1/api/call-flows/${flow_id}`, {
190
+ body: { prompt },
191
+ workspaceId: workspace_id,
192
+ });
193
+ const p = res.pathway ?? res;
194
+ return ok({
195
+ id: p._id,
196
+ promptLength: (p.prompt || "").length,
197
+ promptVersions: (p.promptVersions ?? []).length,
198
+ });
199
+ }));
200
+ server.registerTool("trillet_set_incall_api_tools", {
201
+ title: "Set in-call API tools",
202
+ description: "Replace the flow's in-call API tools (LLM-invokable HTTP calls during the call). Sends the COMPLETE array (arrays replace wholesale). " +
203
+ "GOTCHA: the runtime auto-appends call metadata into request bodies/query params unless callSetting.disableMetadata is true; strict APIs like Airtable reject unknown body keys, so disable_metadata defaults to true here.",
204
+ inputSchema: {
205
+ flow_id: z.string(),
206
+ tools: z.array(incallApiToolSchema).describe("Complete list of in-call API tools"),
207
+ disable_metadata: z
208
+ .boolean()
209
+ .default(true)
210
+ .describe("Set callSetting.disableMetadata (recommended true for strict third-party APIs)"),
211
+ ...workspaceParam,
212
+ },
213
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
214
+ }, guard(async ({ flow_id, tools, disable_metadata, workspace_id }) => {
215
+ const body = { webhooks: tools };
216
+ if (disable_metadata !== undefined) {
217
+ body.settings = { callSetting: { disableMetadata: disable_metadata } };
218
+ }
219
+ const res = await client.request("PUT", `/v1/api/call-flows/${flow_id}`, {
220
+ body,
221
+ workspaceId: workspace_id,
222
+ });
223
+ const p = res.pathway ?? res;
224
+ return ok({
225
+ id: p._id,
226
+ inCallApiTools: (p.webhooks ?? []).map((w) => w.name),
227
+ disableMetadata: p.settings?.callSetting?.disableMetadata,
228
+ promptReminder: 'Reference each tool in the prompt as {{realtime_api "<name>" "<comma-separated variable names>"}}.',
229
+ });
230
+ }));
231
+ server.registerTool("trillet_set_post_call_analysis", {
232
+ title: "Set post-call analysis fields",
233
+ description: "Replace the flow's post-call analysis fields (structured extraction after each call; results land in the call's analyzedData and are available to workflows as {{trigger.analyzedData.<name>}}). The platform always adds summary, call_disposition, and call_sentiment automatically. Tip: make selector options exactly match any downstream system's allowed values.",
234
+ inputSchema: {
235
+ flow_id: z.string(),
236
+ fields: z.array(postCallFieldSchema),
237
+ ...workspaceParam,
238
+ },
239
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
240
+ }, guard(async ({ flow_id, fields, workspace_id }) => {
241
+ const res = await client.request("PUT", `/v1/api/call-flows/${flow_id}`, {
242
+ body: { settings: { postCallAnalysis: fields } },
243
+ workspaceId: workspace_id,
244
+ });
245
+ const p = res.pathway ?? res;
246
+ return ok({
247
+ id: p._id,
248
+ postCallAnalysisFields: (p.settings?.postCallAnalysis ?? []).map((f) => f.name),
249
+ });
250
+ }));
251
+ server.registerTool("trillet_list_drafts", {
252
+ title: "List editor drafts",
253
+ description: "List builder drafts for a call flow. An ACTIVE draft silently overrides production for dashboard test calls and the editor view; it is the number-one cause of 'my changes are not taking effect'.",
254
+ inputSchema: { flow_id: z.string(), ...workspaceParam },
255
+ annotations: { readOnlyHint: true, openWorldHint: true },
256
+ }, guard(async ({ flow_id, workspace_id }) => {
257
+ const res = await client.request("GET", `/v1/api/call-flows/${flow_id}/drafts`, {
258
+ workspaceId: workspace_id,
259
+ });
260
+ const list = Array.isArray(res) ? res : res.drafts ?? [];
261
+ return ok(list.map((d) => ({
262
+ id: d._id,
263
+ name: d.name,
264
+ isActive: d.isActive,
265
+ updatedAt: d.updatedAt,
266
+ })));
267
+ }));
268
+ server.registerTool("trillet_deactivate_drafts", {
269
+ title: "Deactivate drafts (use production)",
270
+ description: "Deactivate all editor drafts on a call flow so test calls and the editor use the production version. Run this after API-side edits if a stale builder tab created a draft. Drafts are recoverable, not deleted. Warn the user that anyone with the flow open in a browser should refresh before editing, or their stale tab may re-create the draft or publish over the changes.",
271
+ inputSchema: { flow_id: z.string(), ...workspaceParam },
272
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
273
+ }, guard(async ({ flow_id, workspace_id }) => {
274
+ const res = await client.request("POST", `/v1/api/call-flows/${flow_id}/drafts/deactivate`, {
275
+ workspaceId: workspace_id,
276
+ });
277
+ return ok(res);
278
+ }));
279
+ server.registerTool("trillet_publish_draft", {
280
+ title: "Publish draft to production",
281
+ description: "Publish a specific draft/version snapshot into the flow's PRODUCTION config (live calls pick it up immediately). Get draft ids from trillet_list_drafts. This overwrites production wholesale with the draft's contents.",
282
+ inputSchema: { flow_id: z.string(), draft_id: z.string(), ...workspaceParam },
283
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
284
+ }, guard(async ({ flow_id, draft_id, workspace_id }) => {
285
+ const res = await client.request("POST", `/v1/api/call-flows/${flow_id}/drafts/${draft_id}/publish`, { workspaceId: workspace_id });
286
+ return ok(res);
287
+ }));
288
+ server.registerTool("trillet_duplicate_call_flow", {
289
+ title: "Duplicate call flow",
290
+ description: "Duplicate a call flow (prompt, settings, in-call API tools, post-call analysis) as a new flow in the same workspace.",
291
+ inputSchema: { flow_id: z.string(), ...workspaceParam },
292
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
293
+ }, guard(async ({ flow_id, workspace_id }) => {
294
+ const res = await client.request("POST", `/v1/api/call-flows/${flow_id}/duplicate`, {
295
+ workspaceId: workspace_id,
296
+ });
297
+ return ok(summarizeFlow(res.pathway ?? res));
298
+ }));
299
+ server.registerTool("trillet_archive_call_flow", {
300
+ title: "Archive / restore call flow",
301
+ description: "Archive a call flow (hides it from the default list and takes it out of use without deleting) or restore an archived one.",
302
+ inputSchema: {
303
+ flow_id: z.string(),
304
+ restore: z.boolean().default(false).describe("true un-archives the flow"),
305
+ ...workspaceParam,
306
+ },
307
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
308
+ }, guard(async ({ flow_id, restore, workspace_id }) => {
309
+ const res = await client.request("PATCH", `/v1/api/call-flows/${flow_id}/${restore ? "restore" : "archive"}`, { workspaceId: workspace_id });
310
+ return ok(res);
311
+ }));
312
+ server.registerTool("trillet_delete_call_flow", {
313
+ title: "Delete call flow",
314
+ description: "Permanently delete a call flow. Prefer trillet_archive_call_flow unless the flow is definitely disposable (e.g. cleaning up a demo). Always confirm with the user; requires confirm=true.",
315
+ inputSchema: {
316
+ flow_id: z.string(),
317
+ confirm: z
318
+ .boolean()
319
+ .describe("Must be true. Only set after the user explicitly approved deleting this flow."),
320
+ ...workspaceParam,
321
+ },
322
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
323
+ }, guard(async ({ flow_id, confirm, workspace_id }) => {
324
+ if (!confirm) {
325
+ return ok({
326
+ blocked: true,
327
+ message: "Refusing to delete: confirm=false. Ask the user to approve deleting flow " +
328
+ flow_id +
329
+ " and retry with confirm=true (or archive it instead).",
330
+ });
331
+ }
332
+ const res = await client.request("DELETE", `/v1/api/call-flows/${flow_id}`, {
333
+ workspaceId: workspace_id,
334
+ });
335
+ return ok(res ?? { deleted: flow_id });
336
+ }));
337
+ }