@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,406 @@
1
+ import { z } from "zod";
2
+ import { ok, guard, workspaceParam } from "../helpers.js";
3
+ const perDaySchema = z
4
+ .record(z.string(), z.object({ enabled: z.boolean(), start: z.string(), end: z.string() }))
5
+ .describe('FULL day names as keys: {"Monday": {enabled, start "HH:MM", end "HH:MM"}, ...}');
6
+ const transferSchema = z
7
+ .object({
8
+ friendly_name: z.string().describe("Unique per flow; how the prompt refers to this transfer"),
9
+ method: z
10
+ .enum(["warm", "cold", "whisper", "agent"])
11
+ .default("cold")
12
+ .describe("The transfer type field is named method, NOT type"),
13
+ destination: z.string().describe("E.164 number, or target agent/pathway id for method=agent"),
14
+ destinationType: z.enum(["SIP", "dynamic", "variable", "agent"]).default("SIP"),
15
+ isSummary: z.boolean().default(true).describe("Speak a summary to the receiving human (warm)"),
16
+ summary_format: z.string().optional(),
17
+ warmTransferTimeout: z.number().optional().describe("Seconds to wait for pickup (15-60 typical)"),
18
+ transferTimeoutAction: z.enum(["end_call", "continue_with_message"]).optional(),
19
+ timeoutMessage: z.string().optional(),
20
+ extension: z.string().optional().describe("DTMF digits dialled after connect"),
21
+ transferGreetingMessage: z.string().optional().describe("Spoken to the caller before transferring"),
22
+ transferInitiationMessage: z.string().optional(),
23
+ conditionGroups: z
24
+ .array(z.object({
25
+ error_message: z.string(),
26
+ conditions: z.array(z.object({
27
+ conditional_variable: z.string().describe('e.g. "_response.customer_id"'),
28
+ operator: z.enum(["exists", "not_empty", "equals", "not_equals", "greater_than", "less_than"]),
29
+ value: z.string().optional(),
30
+ connector: z.enum(["AND", "OR"]).optional(),
31
+ })),
32
+ }))
33
+ .optional()
34
+ .describe("Gate the transfer on call state; error_message is spoken when blocked"),
35
+ officeHoursRestriction: z
36
+ .object({
37
+ enabled: z.boolean(),
38
+ error_message: z.string().optional(),
39
+ specialDays_error_message: z.string().optional(),
40
+ perDay: perDaySchema.optional(),
41
+ specialDays: z.array(z.string()).optional().describe('Blocked dates ["YYYY-MM-DD"]'),
42
+ })
43
+ .optional()
44
+ .describe("BLOCK the transfer outside hours (agent apologises instead)"),
45
+ businessHoursRouting: z
46
+ .object({
47
+ enabled: z.boolean(),
48
+ timezone: z.string().describe("IANA timezone"),
49
+ schedule: perDaySchema.describe("NOTE: key is 'schedule' here, not 'perDay'"),
50
+ workingHours: z.object({
51
+ destinations: z.array(z.string()).describe("Ordered E.164 list, tried sequentially"),
52
+ message: z.string().optional(),
53
+ blocked: z.boolean().optional(),
54
+ blocked_message: z.string().optional(),
55
+ }),
56
+ afterHours: z.object({
57
+ destinations: z.array(z.string()),
58
+ message: z.string().optional(),
59
+ blocked: z.boolean().optional(),
60
+ blocked_message: z.string().optional(),
61
+ }),
62
+ })
63
+ .optional()
64
+ .describe("REROUTE by time of day. Validation: each of workingHours/afterHours needs >=1 destination unless blocked=true, else the save 400s."),
65
+ custom_headers: z
66
+ .array(z.object({ key: z.string(), value: z.string() }))
67
+ .optional()
68
+ .describe("SIP headers; values support {{_metadata.X}} / {{_response.X}} placeholders"),
69
+ })
70
+ .describe("One transfer destination");
71
+ export function registerFlowSettingTools(server, client) {
72
+ const putSettings = async (flow_id, settings, workspace_id) => {
73
+ const res = await client.request("PUT", `/v1/api/call-flows/${flow_id}`, {
74
+ body: { settings },
75
+ workspaceId: workspace_id,
76
+ });
77
+ return res.pathway ?? res;
78
+ };
79
+ server.registerTool("trillet_set_transfers", {
80
+ title: "Set call transfers",
81
+ description: "Replace the flow's transfer destinations (COMPLETE array; arrays replace wholesale). Transfers let the agent hand the call to humans: method warm (announce first), cold (blind), whisper, or agent (another AI flow). To let the model trigger them, also mention each transfer's friendly_name in the prompt. Set enableTransferInstructions true on settings when transfers exist (this tool does it automatically when the list is non-empty).",
82
+ inputSchema: {
83
+ flow_id: z.string(),
84
+ transfers: z.array(transferSchema),
85
+ ...workspaceParam,
86
+ },
87
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
88
+ }, guard(async ({ flow_id, transfers, workspace_id }) => {
89
+ const p = await putSettings(flow_id, { transfers, enableTransferInstructions: transfers.length > 0 }, workspace_id);
90
+ return ok({
91
+ id: p._id,
92
+ transfers: (p.settings?.transfers ?? []).map((t) => `${t.friendly_name} (${t.method} -> ${t.destination})`),
93
+ });
94
+ }));
95
+ server.registerTool("trillet_set_office_hours", {
96
+ title: "Set flow office hours",
97
+ description: "Set the flow-level office hours (settings.OfficeHoursSettings, perDay shape with FULL day names). SEMANTICS: this DEFERS OUTBOUND calls to the next open slot (call shows as scheduled with pendingReason office_hours); it does NOT block inbound calls. To gate transfers by time use officeHoursRestriction/businessHoursRouting on the transfer instead.",
98
+ inputSchema: {
99
+ flow_id: z.string(),
100
+ enabled: z.boolean(),
101
+ timezone: z.string().describe("IANA timezone, e.g. Australia/Melbourne"),
102
+ per_day: perDaySchema,
103
+ blocked_dates: z.array(z.string()).default([]).describe('["YYYY-MM-DD"]'),
104
+ ...workspaceParam,
105
+ },
106
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
107
+ }, guard(async ({ flow_id, enabled, timezone, per_day, blocked_dates, workspace_id }) => {
108
+ const p = await putSettings(flow_id, { OfficeHoursSettings: { enabled, timezone, perDay: per_day, blockedDates: blocked_dates } }, workspace_id);
109
+ return ok({ id: p._id, OfficeHoursSettings: p.settings?.OfficeHoursSettings });
110
+ }));
111
+ server.registerTool("trillet_set_call_settings", {
112
+ title: "Set call settings",
113
+ description: "Update call behavior settings (deep-merges; only sent keys change). Covers silence handling, duration limits, voicemail detection, IVR navigation, recording, LLM cache, transcript redaction, and post-call analysis model.",
114
+ inputSchema: {
115
+ flow_id: z.string(),
116
+ end_call_on_silence: z.number().optional().describe("Seconds of caller silence before ending"),
117
+ silence_reminder_retries: z.number().optional(),
118
+ max_call_duration: z.number().optional().describe("Seconds (default 1800)"),
119
+ max_call_duration_message: z.string().optional(),
120
+ reminder_messages: z.array(z.string()).optional(),
121
+ post_call_analysis_model: z.string().optional().describe('Default "gemini-2.5-flash"'),
122
+ disable_metadata: z.boolean().optional().describe("Stop auto-appending call metadata to in-call API requests (required for strict APIs)"),
123
+ redact_transcription: z.boolean().optional(),
124
+ enable_llm_cache: z.boolean().optional().describe("CAUTION: also defers {{var}} substitution to speak-time"),
125
+ voicemail_detection: z
126
+ .object({
127
+ enabled: z.boolean(),
128
+ action: z.enum(["hangup", "leave_message"]),
129
+ leaveMessage: z.string().optional(),
130
+ amdDetection: z.boolean().optional(),
131
+ amdTimeout: z.number().optional(),
132
+ })
133
+ .optional(),
134
+ ivr_handling: z
135
+ .object({
136
+ enabled: z.boolean(),
137
+ mode: z.enum(["converse", "auto", "sequence"]).describe("sequence requires keySequence"),
138
+ keySequence: z.array(z.string()).optional().describe("Digits/*/# steps, max 10 (only for mode=sequence)"),
139
+ maxAttempts: z.number().optional(),
140
+ dtmfDelaySeconds: z.number().optional(),
141
+ priorityList: z.array(z.string()).optional().describe("What to ask the IVR for, in priority order (auto mode)"),
142
+ holdQueue: z.object({ enabled: z.boolean(), maxWaitSeconds: z.number() }).optional(),
143
+ })
144
+ .optional()
145
+ .describe("Outbound IVR menu navigation. There is no separate dtmfSettings key; DTMF lives here."),
146
+ enable_recording: z.boolean().optional().describe("settings.enableRecording (flow-level)"),
147
+ enable_public_recording: z.boolean().optional(),
148
+ customer_location: z.string().optional().describe("IANA timezone used for time context and timestamps"),
149
+ time_context_enabled: z.boolean().optional().describe("Agent knows current date/time (on unless explicitly false)"),
150
+ utilize_prior_calls_context: z.boolean().optional(),
151
+ ...workspaceParam,
152
+ },
153
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
154
+ }, guard(async (a) => {
155
+ const cs = {};
156
+ if (a.end_call_on_silence !== undefined)
157
+ cs.endCallOnSilence = a.end_call_on_silence;
158
+ if (a.silence_reminder_retries !== undefined)
159
+ cs.silenceReminderRetries = a.silence_reminder_retries;
160
+ if (a.max_call_duration !== undefined)
161
+ cs.maxCallDuration = a.max_call_duration;
162
+ if (a.max_call_duration_message !== undefined)
163
+ cs.maxCallDurationMessage = a.max_call_duration_message;
164
+ if (a.reminder_messages !== undefined)
165
+ cs.reminderMessages = a.reminder_messages;
166
+ if (a.post_call_analysis_model !== undefined)
167
+ cs.postCallAnalysisModel = a.post_call_analysis_model;
168
+ if (a.disable_metadata !== undefined)
169
+ cs.disableMetadata = a.disable_metadata;
170
+ if (a.redact_transcription !== undefined)
171
+ cs.redactTranscription = a.redact_transcription;
172
+ if (a.enable_llm_cache !== undefined)
173
+ cs.enableLlmCache = a.enable_llm_cache;
174
+ if (a.voicemail_detection !== undefined)
175
+ cs.voicemailDetection = a.voicemail_detection;
176
+ if (a.ivr_handling !== undefined)
177
+ cs.ivrHandling = a.ivr_handling;
178
+ const settings = {};
179
+ if (Object.keys(cs).length)
180
+ settings.callSetting = cs;
181
+ if (a.enable_recording !== undefined)
182
+ settings.enableRecording = a.enable_recording;
183
+ if (a.enable_public_recording !== undefined)
184
+ settings.enablePublicRecording = a.enable_public_recording;
185
+ if (a.customer_location !== undefined)
186
+ settings.customerLocation = a.customer_location;
187
+ if (a.time_context_enabled !== undefined)
188
+ settings.timeContextEnabled = a.time_context_enabled;
189
+ if (a.utilize_prior_calls_context !== undefined)
190
+ settings.utilizePriorCallsContext = a.utilize_prior_calls_context;
191
+ const p = await putSettings(a.flow_id, settings, a.workspace_id);
192
+ return ok({ id: p._id, callSetting: p.settings?.callSetting, applied: Object.keys(settings) });
193
+ }));
194
+ server.registerTool("trillet_set_speech_settings", {
195
+ title: "Set speech settings",
196
+ description: "Tune conversational feel: responsiveness (1-10, higher = replies faster), interruptionSensitivity (1-10, higher = easier for the caller to interrupt; 5.5 recommended), vadModel, preemptive generation, TTS pronunciation overrides, and welcome message behavior.",
197
+ inputSchema: {
198
+ flow_id: z.string(),
199
+ responsiveness: z.number().optional(),
200
+ interruption_sensitivity: z.number().optional(),
201
+ vad_model: z.string().optional().describe('"stt" | "default" | "multilingualModel"'),
202
+ enable_preemptive_generation: z.boolean().optional(),
203
+ pronunciation: z
204
+ .array(z.object({ word: z.string(), alias: z.string() }))
205
+ .optional()
206
+ .describe("TTS pronunciation overrides (how the agent SAYS words)"),
207
+ welcome_message_interruptible: z.boolean().optional(),
208
+ welcome_message_delay_ms: z.number().optional().describe("0-5000 ms"),
209
+ ...workspaceParam,
210
+ },
211
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
212
+ }, guard(async (a) => {
213
+ const ss = {};
214
+ if (a.responsiveness !== undefined)
215
+ ss.responsiveness = a.responsiveness;
216
+ if (a.interruption_sensitivity !== undefined)
217
+ ss.interruptionSensitivity = a.interruption_sensitivity;
218
+ if (a.vad_model !== undefined)
219
+ ss.vadModel = a.vad_model;
220
+ if (a.enable_preemptive_generation !== undefined)
221
+ ss.enablePreemptiveGeneration = a.enable_preemptive_generation;
222
+ if (a.pronunciation !== undefined)
223
+ ss.pronunciation = a.pronunciation;
224
+ const settings = {};
225
+ if (Object.keys(ss).length)
226
+ settings.speechSetting = ss;
227
+ if (a.welcome_message_interruptible !== undefined)
228
+ settings.welcomeMessageInterruptible = a.welcome_message_interruptible;
229
+ if (a.welcome_message_delay_ms !== undefined)
230
+ settings.welcomeMessageDelay = a.welcome_message_delay_ms;
231
+ const p = await putSettings(a.flow_id, settings, a.workspace_id);
232
+ return ok({ id: p._id, speechSetting: p.settings?.speechSetting });
233
+ }));
234
+ server.registerTool("trillet_set_welcome_message", {
235
+ title: "Set welcome message",
236
+ description: "Set the greeting for inbound and/or outbound (and web test) calls. selected 'customMessage' speaks the exact text; 'aiFirst' lets the model generate from aiGreetingPrompt. NEVER also script a greeting inside the prompt (causes double greeting). Web/dashboard test calls use the Outbound greeting.",
237
+ inputSchema: {
238
+ flow_id: z.string(),
239
+ inbound_message: z.string().optional(),
240
+ outbound_message: z.string().optional(),
241
+ mode: z.enum(["customMessage", "aiFirst"]).default("customMessage"),
242
+ ai_greeting_prompt: z.string().optional().describe("Only for mode aiFirst"),
243
+ ...workspaceParam,
244
+ },
245
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
246
+ }, guard(async ({ flow_id, inbound_message, outbound_message, mode, ai_greeting_prompt, workspace_id }) => {
247
+ const mk = (msg) => ({
248
+ selected: mode,
249
+ customMessage: msg ?? "",
250
+ aiGreetingPrompt: ai_greeting_prompt ?? "",
251
+ });
252
+ const cfg = {};
253
+ if (inbound_message !== undefined || mode === "aiFirst")
254
+ cfg.Inbound = mk(inbound_message);
255
+ if (outbound_message !== undefined || mode === "aiFirst")
256
+ cfg.Outbound = mk(outbound_message);
257
+ const p = await putSettings(flow_id, { welcomeMessageConfig: cfg }, workspace_id);
258
+ return ok({ id: p._id, welcomeMessageConfig: p.settings?.welcomeMessageConfig });
259
+ }));
260
+ server.registerTool("trillet_set_code_actions", {
261
+ title: "Set code actions (feature-gated)",
262
+ description: "Replace the flow's code actions: Python functions the agent can run mid-call ({name, description, code, variables[{name,description}]}). FEATURE-GATED per workspace: non-whitelisted workspaces get the codeActions SILENTLY STRIPPED (the save returns 200 without them) — this tool verifies persistence and reports if the workspace lacks access.",
263
+ inputSchema: {
264
+ flow_id: z.string(),
265
+ code_actions: z.array(z.object({
266
+ name: z.string(),
267
+ description: z.string(),
268
+ code: z.string().describe("Python source"),
269
+ enabled: z.boolean().default(true),
270
+ variables: z.array(z.object({ name: z.string(), description: z.string() })).default([]),
271
+ })),
272
+ ...workspaceParam,
273
+ },
274
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
275
+ }, guard(async ({ flow_id, code_actions, workspace_id }) => {
276
+ const body = { codeActions: code_actions.map((c) => ({ ...c, language: "python" })) };
277
+ const res = await client.request("PUT", `/v1/api/call-flows/${flow_id}`, {
278
+ body,
279
+ workspaceId: workspace_id,
280
+ });
281
+ const p = res.pathway ?? res;
282
+ const saved = (p.codeActions ?? []).map((c) => c.name);
283
+ const requested = code_actions.map((c) => c.name);
284
+ const stripped = requested.filter((n) => !saved.includes(n));
285
+ return ok({
286
+ id: p._id,
287
+ savedCodeActions: saved,
288
+ ...(stripped.length
289
+ ? {
290
+ warning: `Workspace is not whitelisted for CODE_ACTIONS; these were silently stripped by the API: ${stripped.join(", ")}. Ask Trillet to whitelist the workspace.`,
291
+ }
292
+ : {}),
293
+ });
294
+ }));
295
+ server.registerTool("trillet_set_background_audio", {
296
+ title: "Set background audio",
297
+ description: "Set the flow's background audio (settings.backgroundNoise, deep-merges; only sent keys change): an ambient soundscape played under the whole call, a thinking sound while the model generates, and hold music during transfers, each with its own 0-1 volume. Use ambient_sound \"none\" to switch ambience off.",
298
+ inputSchema: {
299
+ flow_id: z.string(),
300
+ ambient_sound: z
301
+ .string()
302
+ .optional()
303
+ .describe('Ambient background sound: "none", "call_center.mp3", or "quiet_office.mp3"'),
304
+ thinking_sound: z
305
+ .string()
306
+ .optional()
307
+ .describe('Sound played while the AI is thinking: "none" or "keyboard"'),
308
+ hold_sound: z
309
+ .string()
310
+ .optional()
311
+ .describe('Hold music during transfers: only "none" is available (plays the platform Lobby Hold Music)'),
312
+ ambient_volume: z.number().min(0).max(1).optional().describe("Ambient volume 0-1 (default 0.5)"),
313
+ thinking_volume: z.number().min(0).max(1).optional().describe("Thinking-sound volume 0-1 (default 0.5)"),
314
+ hold_volume: z.number().min(0).max(1).optional().describe("Hold-music volume 0-1 (default 0.5)"),
315
+ ...workspaceParam,
316
+ },
317
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
318
+ }, guard(async (a) => {
319
+ const noise = {};
320
+ if (a.ambient_sound !== undefined)
321
+ noise.ambient_sound = a.ambient_sound;
322
+ if (a.thinking_sound !== undefined)
323
+ noise.thinking_sound = a.thinking_sound;
324
+ if (a.hold_sound !== undefined)
325
+ noise.hold_sound = a.hold_sound;
326
+ if (a.ambient_volume !== undefined)
327
+ noise.ambient_volume = a.ambient_volume;
328
+ if (a.thinking_volume !== undefined)
329
+ noise.thinking_volume = a.thinking_volume;
330
+ if (a.hold_volume !== undefined)
331
+ noise.hold_volume = a.hold_volume;
332
+ if (!Object.keys(noise).length)
333
+ return ok({ error: "No changes specified" });
334
+ const p = await putSettings(a.flow_id, { backgroundNoise: noise }, a.workspace_id);
335
+ return ok({ id: p._id, backgroundNoise: p.settings?.backgroundNoise });
336
+ }));
337
+ server.registerTool("trillet_set_security_settings", {
338
+ title: "Set security and access settings",
339
+ description: "Update the flow's security, access and memory settings (deep-merges; only sent keys change). Covers HIPAA compliance (settings.security.hipaaCompliance; enabling it also turns call recording off, as the dashboard does), public access for demo links (settings.security.publicAccess), redirect-incoming-calls-to-a-human (settings.redirectToHuman: forwards inbound calls straight to a number, optionally gated by conditions), and identifier-based web demo memory (settings.identifierBasedWebDemoMemory; needs utilizePriorCallsContext on, see trillet_set_call_settings).",
340
+ inputSchema: {
341
+ flow_id: z.string(),
342
+ hipaa_compliance: z
343
+ .boolean()
344
+ .optional()
345
+ .describe("Enable HIPAA compliance. Enabling also disables enableRecording and enablePublicRecording."),
346
+ public_access: z
347
+ .boolean()
348
+ .optional()
349
+ .describe("Allow public (unauthenticated) access to this flow, e.g. for web demo links"),
350
+ redirect_to_human: z
351
+ .object({
352
+ enabled: z.boolean().optional().describe("Forward inbound calls to a human instead of the AI"),
353
+ phoneNumber: z.string().optional().describe("E.164 number to redirect to"),
354
+ redirectMessageEnabled: z.boolean().optional().describe("Speak a message before redirecting"),
355
+ redirectMessage: z.string().optional().describe("Message spoken before the redirect"),
356
+ conditions: z
357
+ .array(z.object({
358
+ conditional_variable: z.string().describe('Response variable path, e.g. "_response.vip"'),
359
+ operator: z.enum(["exists", "not_empty", "equals", "not_equals", "greater_than", "less_than"]),
360
+ value: z.string().optional().describe("Only for equals/not_equals/greater_than/less_than"),
361
+ error_message: z.string().optional(),
362
+ }))
363
+ .optional()
364
+ .describe("Only redirect when every condition holds (array replaces wholesale)"),
365
+ })
366
+ .optional()
367
+ .describe("settings.redirectToHuman (the dashboard's Redirect incoming calls section)"),
368
+ identifier_based_web_demo_memory: z
369
+ .boolean()
370
+ .optional()
371
+ .describe("Remember web demo visitors by identifier across sessions (requires utilizePriorCallsContext)"),
372
+ ...workspaceParam,
373
+ },
374
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
375
+ }, guard(async (a) => {
376
+ const settings = {};
377
+ const security = {};
378
+ if (a.hipaa_compliance !== undefined) {
379
+ security.hipaaCompliance = a.hipaa_compliance;
380
+ if (a.hipaa_compliance) {
381
+ settings.enableRecording = false;
382
+ settings.enablePublicRecording = false;
383
+ }
384
+ }
385
+ if (a.public_access !== undefined)
386
+ security.publicAccess = a.public_access;
387
+ if (Object.keys(security).length)
388
+ settings.security = security;
389
+ if (a.redirect_to_human !== undefined) {
390
+ settings.redirectToHuman = Object.fromEntries(Object.entries(a.redirect_to_human).filter(([, v]) => v !== undefined));
391
+ }
392
+ if (a.identifier_based_web_demo_memory !== undefined)
393
+ settings.identifierBasedWebDemoMemory = a.identifier_based_web_demo_memory;
394
+ if (!Object.keys(settings).length)
395
+ return ok({ error: "No changes specified" });
396
+ const p = await putSettings(a.flow_id, settings, a.workspace_id);
397
+ return ok({
398
+ id: p._id,
399
+ security: p.settings?.security,
400
+ redirectToHuman: p.settings?.redirectToHuman,
401
+ identifierBasedWebDemoMemory: p.settings?.identifierBasedWebDemoMemory,
402
+ enableRecording: p.settings?.enableRecording,
403
+ applied: Object.keys(settings),
404
+ });
405
+ }));
406
+ }
@@ -0,0 +1,143 @@
1
+ import { z } from "zod";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename, extname } from "node:path";
4
+ import { ok, guard, workspaceParam } from "../helpers.js";
5
+ const MIME = {
6
+ ".pdf": "application/pdf",
7
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
8
+ ".txt": "text/plain",
9
+ ".md": "text/markdown",
10
+ ".csv": "text/csv",
11
+ ".json": "application/json",
12
+ };
13
+ export function registerKnowledgeBaseTools(server, client) {
14
+ server.registerTool("trillet_list_knowledge_bases", {
15
+ title: "List knowledge bases",
16
+ description: "List the workspace's knowledge bases: id, name, description, file/text presence.",
17
+ inputSchema: { ...workspaceParam },
18
+ annotations: { readOnlyHint: true, openWorldHint: true },
19
+ }, guard(async ({ workspace_id }) => {
20
+ const res = await client.request("GET", "/v1/api/knowledgebase/", {
21
+ workspaceId: workspace_id,
22
+ });
23
+ const list = Array.isArray(res) ? res : res.knowledgeBases ?? res.data ?? [];
24
+ return ok(list.map((k) => ({
25
+ id: k._id,
26
+ name: k.name,
27
+ description: k.description,
28
+ files: (k.files ?? []).map((f) => f.fileName ?? f.fileKey ?? f),
29
+ hasText: Boolean(k.textContent),
30
+ updatedAt: k.updatedAt,
31
+ })));
32
+ }));
33
+ server.registerTool("trillet_get_knowledge_base", {
34
+ title: "Get knowledge base",
35
+ description: "Fetch one knowledge base (metadata + file list), optionally with which call flows use it (include_usage).",
36
+ inputSchema: {
37
+ knowledge_base_id: z.string(),
38
+ include_usage: z.boolean().default(false),
39
+ ...workspaceParam,
40
+ },
41
+ annotations: { readOnlyHint: true, openWorldHint: true },
42
+ }, guard(async ({ knowledge_base_id, include_usage, workspace_id }) => {
43
+ const kb = await client.request("GET", `/v1/api/knowledgebase/${knowledge_base_id}`, {
44
+ workspaceId: workspace_id,
45
+ });
46
+ const out = { knowledgeBase: kb.knowledgeBase ?? kb };
47
+ if (include_usage) {
48
+ out.usage = await client
49
+ .request("GET", `/v1/api/knowledgebase/${knowledge_base_id}/usage`, {
50
+ workspaceId: workspace_id,
51
+ })
52
+ .catch((e) => ({ error: String(e?.message ?? e) }));
53
+ }
54
+ return ok(out);
55
+ }));
56
+ server.registerTool("trillet_delete_knowledge_base", {
57
+ title: "Delete knowledge base (or one file)",
58
+ description: "Delete an entire knowledge base, or just one file from it when file_key is given. Check usage first (trillet_get_knowledge_base include_usage=true): flows still pointing at a deleted KB lose retrieval. Always confirm with the user; requires confirm=true.",
59
+ inputSchema: {
60
+ knowledge_base_id: z.string(),
61
+ file_key: z.string().optional().describe("Delete only this file (fileKey) instead of the whole KB"),
62
+ confirm: z
63
+ .boolean()
64
+ .describe("Must be true. Only set after the user explicitly approved the deletion."),
65
+ ...workspaceParam,
66
+ },
67
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
68
+ }, guard(async ({ knowledge_base_id, file_key, confirm, workspace_id }) => {
69
+ if (!confirm) {
70
+ return ok({
71
+ blocked: true,
72
+ message: "Refusing to delete: confirm=false. Ask the user to approve and retry with confirm=true.",
73
+ });
74
+ }
75
+ const path = file_key
76
+ ? `/v1/api/knowledgebase/knowledgeBases/${knowledge_base_id}/files/${file_key}`
77
+ : `/v1/api/knowledgebase/${knowledge_base_id}`;
78
+ const res = await client.request("DELETE", path, { workspaceId: workspace_id });
79
+ return ok(res ?? { deleted: file_key ?? knowledge_base_id });
80
+ }));
81
+ server.registerTool("trillet_create_knowledge_base", {
82
+ title: "Create knowledge base",
83
+ description: "Create an empty knowledge base. Add content with trillet_add_kb_text or trillet_upload_kb_file, then attach it to a call flow with trillet_attach_knowledge_bases.",
84
+ inputSchema: {
85
+ name: z.string(),
86
+ description: z.string().optional(),
87
+ ...workspaceParam,
88
+ },
89
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
90
+ }, guard(async ({ name, description, workspace_id }) => {
91
+ const res = await client.request("POST", "/v1/api/knowledgebase/", {
92
+ body: { name, description },
93
+ workspaceId: workspace_id,
94
+ });
95
+ return ok(res);
96
+ }));
97
+ server.registerTool("trillet_add_kb_text", {
98
+ title: "Add text to knowledge base",
99
+ description: "Add plain-text content to a knowledge base. The text is chunked and vector-embedded immediately.",
100
+ inputSchema: {
101
+ knowledge_base_id: z.string(),
102
+ text: z.string().describe("The content to embed"),
103
+ name: z.string().optional().describe("Label for this text block"),
104
+ ...workspaceParam,
105
+ },
106
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
107
+ }, guard(async ({ knowledge_base_id, text, name, workspace_id }) => {
108
+ const res = await client.request("POST", `/v1/api/knowledgebase/${knowledge_base_id}/text`, { body: { text, name }, workspaceId: workspace_id });
109
+ return ok(res);
110
+ }));
111
+ server.registerTool("trillet_upload_kb_file", {
112
+ title: "Upload file to knowledge base",
113
+ description: "Upload a local file (PDF, DOCX, TXT, MD, CSV, JSON) to a knowledge base. Text is extracted and vector-embedded synchronously in the same request. GOTCHA: each knowledge base holds ONE file; uploading replaces the existing file. Use separate knowledge bases for separate documents.",
114
+ inputSchema: {
115
+ knowledge_base_id: z.string(),
116
+ file_path: z.string().describe("Absolute path to the local file"),
117
+ ...workspaceParam,
118
+ },
119
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
120
+ }, guard(async ({ knowledge_base_id, file_path, workspace_id }) => {
121
+ const data = await readFile(file_path);
122
+ const mimetype = MIME[extname(file_path).toLowerCase()] ?? "application/octet-stream";
123
+ const res = await client.upload(`/v1/api/knowledgebase/${knowledge_base_id}/files`, "uploadedFile", basename(file_path), data, mimetype, workspace_id);
124
+ return ok(res);
125
+ }));
126
+ server.registerTool("trillet_attach_knowledge_bases", {
127
+ title: "Attach knowledge bases to call flow",
128
+ description: "Set the COMPLETE list of knowledge base ids on a call flow (the list replaces the previous one; pass an empty array to detach all). The runtime automatically retrieves relevant content during calls.",
129
+ inputSchema: {
130
+ flow_id: z.string(),
131
+ knowledge_base_ids: z.array(z.string()),
132
+ ...workspaceParam,
133
+ },
134
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
135
+ }, guard(async ({ flow_id, knowledge_base_ids, workspace_id }) => {
136
+ const res = await client.request("PUT", `/v1/api/call-flows/${flow_id}`, {
137
+ body: { settings: { knowledgeBases: knowledge_base_ids } },
138
+ workspaceId: workspace_id,
139
+ });
140
+ const p = res.pathway ?? res;
141
+ return ok({ id: p._id, knowledgeBases: p.settings?.knowledgeBases });
142
+ }));
143
+ }