@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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/client.js +116 -0
- package/dist/helpers.js +48 -0
- package/dist/index.js +64 -0
- package/dist/registry.js +106 -0
- package/dist/textcall/deps.js +80 -0
- package/dist/textcall/session.js +209 -0
- package/dist/textcall/worker.js +101 -0
- package/dist/tools/account.js +86 -0
- package/dist/tools/agents.js +178 -0
- package/dist/tools/callFlows.js +337 -0
- package/dist/tools/calls.js +391 -0
- package/dist/tools/flowSettings.js +406 -0
- package/dist/tools/knowledgeBases.js +143 -0
- package/dist/tools/nodeFlows.js +317 -0
- package/dist/tools/phoneNumbers.js +221 -0
- package/dist/tools/secrets.js +45 -0
- package/dist/tools/textCalls.js +223 -0
- package/dist/tools/workflows.js +415 -0
- package/dist/version.js +7 -0
- package/package.json +52 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, guard, workspaceParam } from "../helpers.js";
|
|
3
|
+
const NODE_FLOW_REFERENCE = `# Trillet Node Flow Reference (verified against the runtime)
|
|
4
|
+
|
|
5
|
+
Node flows are graph-based call flows (pathway.promptType "nodes"). The graph lives in its own
|
|
6
|
+
NodeGraph document; the companion agent and pathway are AUTO-CREATED when you create a node flow.
|
|
7
|
+
Edits save to the live graph WITHOUT validation; calls only ever run the last PUBLISHED version.
|
|
8
|
+
Lifecycle: create -> update graph -> validate -> publish -> call. An unpublished flow fails calls
|
|
9
|
+
with NODE_FLOW_NOT_PUBLISHED.
|
|
10
|
+
|
|
11
|
+
## Graph document fields you can update
|
|
12
|
+
- startNodeId: entry node id (falls back to the only node without incoming edges)
|
|
13
|
+
- variables[]: { key, label, type: string|number|boolean|date|time|enum|phone|email|object|array,
|
|
14
|
+
enumValues[], validation{pattern,min,max,minLength,maxLength}, required, extractionPrompt, redact }
|
|
15
|
+
Every requiredFields/resultVariable key MUST be declared here (UNDECLARED_VARIABLE otherwise).
|
|
16
|
+
- globals: { persona (the node-flow analogue of a prompt), guardrails,
|
|
17
|
+
knowledgeBases[knowledge base ids],
|
|
18
|
+
interrupts[{ id, name, utterancePatterns[], targetNodeId, returnAfter }] }
|
|
19
|
+
Interrupts are GRAPH-GLOBAL jumps (e.g. "speak to a human" from anywhere); returnAfter resumes
|
|
20
|
+
where the caller was. There is no interrupt node type.
|
|
21
|
+
- router: { model, maxCandidates=8, historyTurns=4, confidenceFloor=0.5 }
|
|
22
|
+
- nodes[], edges[]
|
|
23
|
+
|
|
24
|
+
## Node types (11)
|
|
25
|
+
Every node: { nodeId, type, name, config{}, behaviour{}, onError: fallback|end|transfer, maxTurns=8 }
|
|
26
|
+
behaviour (per-node overrides, only set what you mean): temperature (0-2), model,
|
|
27
|
+
blockInterruptions, interruptionThreshold, responseEagerness, resumptionSpeed, reminderSeconds,
|
|
28
|
+
reminderTimes, disableSilenceRepeat, skipUserResponse, overridesInterrupts, excludeFromHistory.
|
|
29
|
+
|
|
30
|
+
- conversation ("Talk"): config { objective (REQUIRED), prompt, requiredFields[varKey],
|
|
31
|
+
exitCriteria, skipIfSatisfied, confirmBeforeExit, clearFieldsOnReentry, knowledgeBases[],
|
|
32
|
+
knowledgeBaseInstruction, abilities[{toolName, instruction}], examples[{caller, agent}] }.
|
|
33
|
+
Abilities are mid-turn tools: cal.com/Google/GHL/ModMed/ServiceM8 calendar actions,
|
|
34
|
+
get_time_for_timezone, next_available_callback_time. NOT transfer_call or end_call.
|
|
35
|
+
- say: config { text (REQUIRED), interruptible (default true) }. Speaks then moves on. Set
|
|
36
|
+
interruptible false to pin a line that must land whole (disclaimer, callback number).
|
|
37
|
+
API/MCP only - there is no editor control for it.
|
|
38
|
+
- decision ("Rule"): no config; routes purely via condition edges. EVERY condition row needs a
|
|
39
|
+
conditional_variable: a row without one is treated as TRUE, and since condition edges are
|
|
40
|
+
evaluated first, that silently makes the edge an unconditional exit taken on every turn.
|
|
41
|
+
Nothing validates this.
|
|
42
|
+
- classify ("Choice"): config { question, options: [{handle, description}] } (>=2, unique handles).
|
|
43
|
+
Each option handle needs its own out-edge.
|
|
44
|
+
- tool ("Action"): config { toolName, arguments{}, resultVariable, retry{attempts 0-3, backoffMs} }.
|
|
45
|
+
Allowed toolNames: realtime_api, execute_code_action, search_knowledge_base, transfer_call,
|
|
46
|
+
end_call, get_time_for_timezone, next_available_callback_time, trigger_workflow.
|
|
47
|
+
TRANSFERS ARE A TOOL NODE with toolName "transfer_call". Outputs: success | error handles.
|
|
48
|
+
- end: config { message, disposition (free string, e.g. "completed"), messageMode:
|
|
49
|
+
instruction|verbatim }. Terminal; no outgoing edges allowed. messageMode defaults to
|
|
50
|
+
instruction (the message is a directive the model words); verbatim speaks it exactly.
|
|
51
|
+
Leave message empty and the runtime generates a brief goodbye instead - EXCEPT when the
|
|
52
|
+
same settle already spoke, so Say("goodbye") -> end does not say goodbye twice. Farewells
|
|
53
|
+
always play to completion (not interruptible). ALWAYS set a disposition: it is not
|
|
54
|
+
validated and defaults to null, which leaves outcome reporting blank.
|
|
55
|
+
- wait ("Listen"): config { requiredFields[] (REQUIRED non-empty), prompt }. Collects without talking much.
|
|
56
|
+
- subflow: config { nodeGraphId (REQUIRED), returnAfter }. returnAfter outputs: returned | error.
|
|
57
|
+
- code: config { code (REQUIRED, JavaScript), resultVariable, arguments }. Runs on Cloudflare
|
|
58
|
+
Workers deployed at publish time; no local fallback. Outputs: success | error.
|
|
59
|
+
- parallel: config { tools: [>=2 of {toolName, arguments, resultVariable}], joinPolicy:
|
|
60
|
+
all_success|any_success }. Outputs: success | error.
|
|
61
|
+
- ivr: ENTRY-ONLY node (pickup-time menu navigation before the graph starts; must have NO incoming
|
|
62
|
+
edges, max one per graph): config { mode: sequence|auto, keySequence[] (digits/*/#),
|
|
63
|
+
priorityList[], holdQueue{enabled, maxWaitSeconds 15-900}, screeningAction: respond|hangup }.
|
|
64
|
+
Optional outputs: human | exhausted | voicemail (unwired outcomes fall back to startNodeId).
|
|
65
|
+
|
|
66
|
+
## Edges
|
|
67
|
+
{ id (REQUIRED), source, target, sourceHandle?, kind: auto|condition|intent|fallback, label,
|
|
68
|
+
condition, priority }
|
|
69
|
+
- kind=auto: unconditional, no model call. kind=condition: deterministic on call state.
|
|
70
|
+
kind=intent: the router model matches on the edge LABEL (label required).
|
|
71
|
+
kind=fallback: taken when nothing else matches (max one per node; collection nodes should have one).
|
|
72
|
+
- Evaluation order: condition (ascending priority) -> auto -> intent -> fallback.
|
|
73
|
+
- Nodes with declared output handles (classify, tool, subflow, code, parallel) REQUIRE
|
|
74
|
+
sourceHandle on their out-edges and each handle wires at most one edge.
|
|
75
|
+
- Cycles are legal (bounded by maxTurns and chain guards).
|
|
76
|
+
|
|
77
|
+
## Condition syntax
|
|
78
|
+
Operators: exists, not_empty, equals, not_equals, equals_ci, not_equals_ci, contains,
|
|
79
|
+
not_contains, greater_than, less_than. Values/fields use placeholders:
|
|
80
|
+
{{_variable.x}} (extracted -> response -> dynamic -> metadata precedence), {{_response.x}},
|
|
81
|
+
{{_dynamic_variable.x}}, {{_metadata.x}}, {{_time_context.currentDate|currentTime|currentDay}}.
|
|
82
|
+
The runtime seeds {{_dynamic_variable.call_direction}} ("inbound" | "outbound") before the start
|
|
83
|
+
chain runs, so a condition edge at the start node can branch inbound vs outbound (the editor's
|
|
84
|
+
Simulate-as-inbound/outbound toggle resolves it too; web calls count as outbound). A per-call
|
|
85
|
+
dynamic variable named call_direction overrides the seeded value.
|
|
86
|
+
For a value the flow COLLECTED, use {{_variable.key}}. A bare {{key}} resolves against dynamic
|
|
87
|
+
variables ONLY, so a bare placeholder for a collected value never matches, with no warning.
|
|
88
|
+
A conditional_variable that is not a {{...}} placeholder is compared as a literal string
|
|
89
|
+
(so "exists" on it is permanently true).
|
|
90
|
+
|
|
91
|
+
## Greeting (the first line)
|
|
92
|
+
Node flows do NOT run the simple-flow welcome pipeline; the first conversational node opens the
|
|
93
|
+
call, honoring pathway welcomeMessageConfig[Inbound|Outbound] (set via trillet_set_welcome_message
|
|
94
|
+
on the companion pathway): selected "customMessage" speaks the configured text verbatim (dynamic
|
|
95
|
+
variables substituted); "userFirst" stays silent so the caller speaks first (use this on Outbound
|
|
96
|
+
to wait for the callee's "hello"); "aiFirst" or unset (the default on auto-created node-flow
|
|
97
|
+
pathways) generates the opening from the landing node's objective. To script a greeting inside
|
|
98
|
+
the graph instead, instruct the start conversation node to speak it word-for-word; do not ALSO
|
|
99
|
+
set a customMessage or the call greets twice.
|
|
100
|
+
|
|
101
|
+
## Common blocking validation errors
|
|
102
|
+
CONVERSATION_WITHOUT_OBJECTIVE, REQUIRED_FIELD_UNDECLARED, UNDECLARED_VARIABLE, NO_START_NODE,
|
|
103
|
+
NO_OUTGOING (non-terminal node with no exits), MISSING_FALLBACK, HANDLE_MISSING/UNKNOWN_HANDLE
|
|
104
|
+
(declared-output nodes), CONDITION_EDGE_WITHOUT_CONDITION, INTENT_EDGE_WITHOUT_LABEL,
|
|
105
|
+
TERMINAL_HAS_OUTGOING, TOOL_NOT_ALLOWED, WAIT_WITHOUT_FIELDS, CLASSIFY_DUPLICATE_HANDLE.
|
|
106
|
+
Warnings (do not block publish): UNREACHABLE_NODE, NO_END_NODE, TOOL_ERROR_PATH_UNWIRED,
|
|
107
|
+
CLASSIFY_NO_FALLBACK, END_WITHOUT_MESSAGE.
|
|
108
|
+
|
|
109
|
+
## Gotchas
|
|
110
|
+
- PUT and draft saves NEVER validate; run validate before publish to catch problems early.
|
|
111
|
+
- Publish snapshots a version, compiles the graph, deploys code nodes to Cloudflare, and is what
|
|
112
|
+
live calls consume. Editing after publish changes nothing on calls until the next publish.
|
|
113
|
+
- pathway.settings (voice, transfers config, office hours, recording, speech settings) still
|
|
114
|
+
applies to node flows unchanged; configure it via the call-flow tools using the companion
|
|
115
|
+
pathway id returned by get.
|
|
116
|
+
- end node disposition is a hint; keep dispositions consistent (e.g. completed, transferred,
|
|
117
|
+
voicemail) for reporting.`;
|
|
118
|
+
function summarizeGraph(g) {
|
|
119
|
+
return {
|
|
120
|
+
id: g._id,
|
|
121
|
+
name: g.name,
|
|
122
|
+
status: g.status,
|
|
123
|
+
startNodeId: g.startNodeId,
|
|
124
|
+
nodes: (g.nodes ?? []).map((n) => `${n.nodeId}(${n.type})`),
|
|
125
|
+
edges: (g.edges ?? []).length,
|
|
126
|
+
variables: (g.variables ?? []).map((v) => v.key),
|
|
127
|
+
interrupts: (g.globals?.interrupts ?? []).map((i) => i.name),
|
|
128
|
+
hasDraft: g.hasDraft,
|
|
129
|
+
publishedVersion: g.publishedVersion,
|
|
130
|
+
publishedAt: g.publishedAt,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function registerNodeFlowTools(server, client) {
|
|
134
|
+
server.registerTool("trillet_node_flow_reference", {
|
|
135
|
+
title: "Node flow reference",
|
|
136
|
+
description: "Return the complete reference for Trillet node flows (graph call flows): node types and their config fields, edge kinds and handle rules, condition syntax, validation errors, and lifecycle gotchas. READ THIS FIRST before creating or editing any node graph.",
|
|
137
|
+
inputSchema: {},
|
|
138
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
139
|
+
}, guard(async () => ok(NODE_FLOW_REFERENCE)));
|
|
140
|
+
server.registerTool("trillet_list_node_flows", {
|
|
141
|
+
title: "List node flows",
|
|
142
|
+
description: "List node-graph call flows in the workspace (summaries; graphs excluded).",
|
|
143
|
+
inputSchema: {
|
|
144
|
+
page: z.number().default(1),
|
|
145
|
+
limit: z.number().default(20),
|
|
146
|
+
...workspaceParam,
|
|
147
|
+
},
|
|
148
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
149
|
+
}, guard(async ({ page, limit, workspace_id }) => {
|
|
150
|
+
const res = await client.request("GET", "/v2/api/node-graphs/", {
|
|
151
|
+
query: { page, limit },
|
|
152
|
+
workspaceId: workspace_id,
|
|
153
|
+
});
|
|
154
|
+
const list = res.docs ?? res.data ?? (Array.isArray(res) ? res : []);
|
|
155
|
+
return ok(list.map((g) => ({
|
|
156
|
+
id: g._id,
|
|
157
|
+
name: g.name,
|
|
158
|
+
status: g.status,
|
|
159
|
+
publishedVersion: g.publishedVersion,
|
|
160
|
+
hasDraft: g.hasDraft,
|
|
161
|
+
stats: g.stats,
|
|
162
|
+
})));
|
|
163
|
+
}));
|
|
164
|
+
server.registerTool("trillet_get_node_flow", {
|
|
165
|
+
title: "Get node flow",
|
|
166
|
+
description: "Fetch a node flow: the graph (nodes, edges, variables, globals, router), plus the companion pathway (its id is what the call-flow settings tools operate on).",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
graph_id: z.string(),
|
|
169
|
+
full_graph: z
|
|
170
|
+
.boolean()
|
|
171
|
+
.default(false)
|
|
172
|
+
.describe("true returns complete nodes/edges configs; false returns a summary"),
|
|
173
|
+
...workspaceParam,
|
|
174
|
+
},
|
|
175
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
176
|
+
}, guard(async ({ graph_id, full_graph, workspace_id }) => {
|
|
177
|
+
const res = await client.request("GET", `/v2/api/node-graphs/${graph_id}`, {
|
|
178
|
+
workspaceId: workspace_id,
|
|
179
|
+
});
|
|
180
|
+
const g = res.graph ?? res;
|
|
181
|
+
const pathway = res.pathway
|
|
182
|
+
? {
|
|
183
|
+
id: res.pathway._id,
|
|
184
|
+
name: res.pathway.name,
|
|
185
|
+
direction: res.pathway.direction,
|
|
186
|
+
agentId: typeof res.pathway.agent === "object" ? res.pathway.agent?._id : res.pathway.agent,
|
|
187
|
+
}
|
|
188
|
+
: null;
|
|
189
|
+
return ok({
|
|
190
|
+
graph: full_graph ? g : summarizeGraph(g),
|
|
191
|
+
pathway,
|
|
192
|
+
});
|
|
193
|
+
}));
|
|
194
|
+
server.registerTool("trillet_create_node_flow", {
|
|
195
|
+
title: "Create node flow",
|
|
196
|
+
description: "Create a new node-graph call flow. This ALSO auto-creates a companion agent and pathway. Optionally start from a platform template (see trillet_list_node_flow_templates). After creating: update the graph, validate, then PUBLISH (calls fail with NODE_FLOW_NOT_PUBLISHED until published).",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
name: z.string(),
|
|
199
|
+
description: z.string().optional(),
|
|
200
|
+
template: z.string().optional().describe("Template key from the templates list"),
|
|
201
|
+
...workspaceParam,
|
|
202
|
+
},
|
|
203
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
204
|
+
}, guard(async ({ name, description, template, workspace_id }) => {
|
|
205
|
+
const body = { name };
|
|
206
|
+
if (description)
|
|
207
|
+
body.description = description;
|
|
208
|
+
if (template)
|
|
209
|
+
body.template = template;
|
|
210
|
+
const res = await client.request("POST", "/v2/api/node-graphs/", {
|
|
211
|
+
body,
|
|
212
|
+
workspaceId: workspace_id,
|
|
213
|
+
});
|
|
214
|
+
const g = res.graph ?? res;
|
|
215
|
+
return ok({ created: summarizeGraph(g), pathwayId: res.pathway?._id ?? g.pathwayId });
|
|
216
|
+
}));
|
|
217
|
+
server.registerTool("trillet_list_node_flow_templates", {
|
|
218
|
+
title: "List node flow templates",
|
|
219
|
+
description: "List available node-flow starter templates.",
|
|
220
|
+
inputSchema: { ...workspaceParam },
|
|
221
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
222
|
+
}, guard(async ({ workspace_id }) => {
|
|
223
|
+
const res = await client.request("GET", "/v2/api/node-graphs/templates", {
|
|
224
|
+
workspaceId: workspace_id,
|
|
225
|
+
});
|
|
226
|
+
return ok(res.templates ?? res);
|
|
227
|
+
}));
|
|
228
|
+
server.registerTool("trillet_update_node_graph", {
|
|
229
|
+
title: "Update node graph",
|
|
230
|
+
description: "Update graph fields: name, description, startNodeId, variables, globals, router, nodes, edges. Arrays replace wholesale, so send complete nodes/edges lists. IMPORTANT: updates are saved WITHOUT validation, and live calls keep running the previously PUBLISHED version. Always trillet_validate_node_graph then trillet_publish_node_graph afterwards. Consult trillet_node_flow_reference for node/edge schemas before building.",
|
|
231
|
+
inputSchema: {
|
|
232
|
+
graph_id: z.string(),
|
|
233
|
+
update: z
|
|
234
|
+
.record(z.string(), z.unknown())
|
|
235
|
+
.describe("Any of: name, description, startNodeId, variables, globals, router, nodes, edges"),
|
|
236
|
+
...workspaceParam,
|
|
237
|
+
},
|
|
238
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
239
|
+
}, guard(async ({ graph_id, update, workspace_id }) => {
|
|
240
|
+
const res = await client.request("PUT", `/v2/api/node-graphs/${graph_id}`, {
|
|
241
|
+
body: update,
|
|
242
|
+
workspaceId: workspace_id,
|
|
243
|
+
});
|
|
244
|
+
return ok(summarizeGraph(res.graph ?? res));
|
|
245
|
+
}));
|
|
246
|
+
server.registerTool("trillet_validate_node_graph", {
|
|
247
|
+
title: "Validate node graph",
|
|
248
|
+
description: "Validate the saved graph (or a proposed unsaved graph passed inline). Returns problems with severity: 'error' blocks publish, 'warning' does not. Problem codes are documented in trillet_node_flow_reference.",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
graph_id: z.string(),
|
|
251
|
+
graph: z
|
|
252
|
+
.record(z.string(), z.unknown())
|
|
253
|
+
.optional()
|
|
254
|
+
.describe("Optional full graph object to validate without saving"),
|
|
255
|
+
...workspaceParam,
|
|
256
|
+
},
|
|
257
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
258
|
+
}, guard(async ({ graph_id, graph, workspace_id }) => {
|
|
259
|
+
const res = await client.request("POST", `/v2/api/node-graphs/${graph_id}/validate`, {
|
|
260
|
+
body: graph ? { graph } : {},
|
|
261
|
+
workspaceId: workspace_id,
|
|
262
|
+
});
|
|
263
|
+
return ok(res);
|
|
264
|
+
}));
|
|
265
|
+
server.registerTool("trillet_publish_node_graph", {
|
|
266
|
+
title: "Publish node graph",
|
|
267
|
+
description: "Publish the graph: validates (400 NODE_GRAPH_INVALID with problems if blocking errors), snapshots a version, compiles, deploys any code nodes to Cloudflare, and makes it live for calls. This is the ONLY way edits reach live calls.",
|
|
268
|
+
inputSchema: {
|
|
269
|
+
graph_id: z.string(),
|
|
270
|
+
note: z.string().optional().describe("Version note"),
|
|
271
|
+
...workspaceParam,
|
|
272
|
+
},
|
|
273
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
274
|
+
}, guard(async ({ graph_id, note, workspace_id }) => {
|
|
275
|
+
const res = await client.request("POST", `/v2/api/node-graphs/${graph_id}/publish`, {
|
|
276
|
+
body: note ? { note } : {},
|
|
277
|
+
workspaceId: workspace_id,
|
|
278
|
+
});
|
|
279
|
+
return ok({
|
|
280
|
+
published: summarizeGraph(res.graph ?? res),
|
|
281
|
+
warnings: res.problems ?? [],
|
|
282
|
+
});
|
|
283
|
+
}));
|
|
284
|
+
server.registerTool("trillet_simulate_node_graph", {
|
|
285
|
+
title: "Simulate node graph",
|
|
286
|
+
description: "Run a text simulation of the graph without a live call: pass caller turns and optional initial variable state; returns the traversal trace. Verifies STRUCTURE (edges wire up, conditions evaluate, handles resolve, guards fire) - it does NOT verify routing quality, because the router is mocked and intent edges are not judged by a model. To test real routing/extraction conversationally, use trillet_start_text_call (runs the live runtime).",
|
|
287
|
+
inputSchema: {
|
|
288
|
+
graph_id: z.string(),
|
|
289
|
+
turns: z.array(z.string()).describe("Caller utterances in order"),
|
|
290
|
+
initial_state: z.record(z.string(), z.unknown()).optional(),
|
|
291
|
+
...workspaceParam,
|
|
292
|
+
},
|
|
293
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
294
|
+
}, guard(async ({ graph_id, turns, initial_state, workspace_id }) => {
|
|
295
|
+
const res = await client.request("POST", `/v2/api/node-graphs/${graph_id}/simulate`, {
|
|
296
|
+
body: { turns, initialState: initial_state ?? {} },
|
|
297
|
+
workspaceId: workspace_id,
|
|
298
|
+
});
|
|
299
|
+
return ok(res);
|
|
300
|
+
}));
|
|
301
|
+
server.registerTool("trillet_list_node_flow_runs", {
|
|
302
|
+
title: "List node flow runs",
|
|
303
|
+
description: "List recent runs (call traversals) of a node graph: path taken, disposition, counters. Use for debugging why calls routed the way they did.",
|
|
304
|
+
inputSchema: {
|
|
305
|
+
graph_id: z.string(),
|
|
306
|
+
run_id: z.string().optional().describe("Fetch one run with full trace events"),
|
|
307
|
+
...workspaceParam,
|
|
308
|
+
},
|
|
309
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
310
|
+
}, guard(async ({ graph_id, run_id, workspace_id }) => {
|
|
311
|
+
const path = run_id
|
|
312
|
+
? `/v2/api/node-graphs/${graph_id}/runs/${run_id}`
|
|
313
|
+
: `/v2/api/node-graphs/${graph_id}/runs`;
|
|
314
|
+
const res = await client.request("GET", path, { workspaceId: workspace_id });
|
|
315
|
+
return ok(res);
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, guard, workspaceParam } from "../helpers.js";
|
|
3
|
+
const officeHoursNumberSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
enabled: z.boolean(),
|
|
6
|
+
start: z.string().describe('"HH:MM"'),
|
|
7
|
+
end: z.string().describe('"HH:MM"'),
|
|
8
|
+
timezone: z.string().describe("IANA timezone"),
|
|
9
|
+
allowedDays: z
|
|
10
|
+
.array(z.string())
|
|
11
|
+
.describe('ABBREVIATED day names: ["Mon","Tue","Wed","Thu","Fri"] (number-level format)'),
|
|
12
|
+
blockedDates: z.array(z.string()).default([]).describe('["YYYY-MM-DD"]'),
|
|
13
|
+
})
|
|
14
|
+
.describe("Phone-number-level office hours (flat shape with abbreviated day names)");
|
|
15
|
+
export function registerPhoneNumberTools(server, client) {
|
|
16
|
+
server.registerTool("trillet_list_phone_numbers", {
|
|
17
|
+
title: "List phone numbers",
|
|
18
|
+
description: "List the workspace's phone numbers: number, status, provider, bound agent/flow, recording, office hours, label.",
|
|
19
|
+
inputSchema: { ...workspaceParam },
|
|
20
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
21
|
+
}, guard(async ({ workspace_id }) => {
|
|
22
|
+
const res = await client.request("GET", "/v1/api/twilio/user-phone-numbers", {
|
|
23
|
+
workspaceId: workspace_id,
|
|
24
|
+
});
|
|
25
|
+
const list = Array.isArray(res) ? res : [];
|
|
26
|
+
return ok(list.map((n) => ({
|
|
27
|
+
id: n._id,
|
|
28
|
+
phoneNumber: n.phoneNumber,
|
|
29
|
+
label: n.label,
|
|
30
|
+
status: n.status,
|
|
31
|
+
provider: n.provider,
|
|
32
|
+
flowType: n.flowType,
|
|
33
|
+
agent: n.agentId ? { id: n.agentId._id ?? n.agentId, name: n.agentId.name } : null,
|
|
34
|
+
callFlowId: n.pathwayId?._id ?? n.pathwayId ?? null,
|
|
35
|
+
recordingEnabled: n.recordingEnabled,
|
|
36
|
+
officeHours: n.officeHours,
|
|
37
|
+
metadataTags: n.metadataTags,
|
|
38
|
+
secretBindings: n.secretBindings,
|
|
39
|
+
monthlyPrice: n.monthlyPrice,
|
|
40
|
+
})));
|
|
41
|
+
}));
|
|
42
|
+
server.registerTool("trillet_search_available_numbers", {
|
|
43
|
+
title: "Search available numbers",
|
|
44
|
+
description: "Search purchasable phone numbers. type is required except for AU. Toll-free numbers are not purchasable.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
country: z.string().describe('ISO country, e.g. "US", "AU", "GB"'),
|
|
47
|
+
type: z.string().optional().describe('"local" | "mobile" | "tollfree" (varies by country)'),
|
|
48
|
+
area_code: z.string().optional(),
|
|
49
|
+
contains: z.string().optional().describe("Digit pattern the number should contain"),
|
|
50
|
+
limit: z.number().default(10),
|
|
51
|
+
...workspaceParam,
|
|
52
|
+
},
|
|
53
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
54
|
+
}, guard(async ({ country, type, area_code, contains, limit, workspace_id }) => {
|
|
55
|
+
const res = await client.request("GET", "/v1/api/twilio/available-numbers", {
|
|
56
|
+
query: { country, type, areaCode: area_code, contains, limit },
|
|
57
|
+
workspaceId: workspace_id,
|
|
58
|
+
});
|
|
59
|
+
return ok(res);
|
|
60
|
+
}));
|
|
61
|
+
server.registerTool("trillet_purchase_phone_number", {
|
|
62
|
+
title: "Purchase phone number (COSTS MONEY)",
|
|
63
|
+
description: "Purchase a phone number found via search. Free-plan allocations complete immediately (201 with mode 'free'); otherwise the response contains a Stripe Checkout URL the user must open to pay. Requires confirm=true after the user explicitly approves the purchase. type required except AU; addressSid required outside US/CA/AU/GB.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
country: z.string(),
|
|
66
|
+
phone_number: z.string().describe("Exact E.164 number from search results"),
|
|
67
|
+
type: z.string().optional(),
|
|
68
|
+
address_sid: z.string().optional(),
|
|
69
|
+
confirm: z.boolean().describe("Must be true. Only after the user explicitly approved."),
|
|
70
|
+
...workspaceParam,
|
|
71
|
+
},
|
|
72
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
73
|
+
}, guard(async ({ country, phone_number, type, address_sid, confirm, workspace_id }) => {
|
|
74
|
+
if (!confirm) {
|
|
75
|
+
return ok({
|
|
76
|
+
blocked: true,
|
|
77
|
+
message: "Refusing: confirm=false. Purchasing a number bills the workspace. Ask the user first.",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const body = { country, phoneNumber: phone_number };
|
|
81
|
+
if (type)
|
|
82
|
+
body.type = type;
|
|
83
|
+
if (address_sid)
|
|
84
|
+
body.addressSid = address_sid;
|
|
85
|
+
const res = await client.request("POST", "/v1/api/twilio/purchase-number", {
|
|
86
|
+
body,
|
|
87
|
+
workspaceId: workspace_id,
|
|
88
|
+
});
|
|
89
|
+
return ok(res);
|
|
90
|
+
}));
|
|
91
|
+
server.registerTool("trillet_bind_phone_number", {
|
|
92
|
+
title: "Bind phone number to agent",
|
|
93
|
+
description: "Bind a workspace phone number to an agent so calls route to it (sets the number active, provisions SIP trunks for the flow's direction). The agent MUST already have a call flow. Omitted optional fields keep their current values. metadataTags values become {{_metadata.<key>}} in prompts/tools; secretBindings maps alias -> workspace secret NAME (both slug-safe lowercase) enabling per-number {{_secret.<alias>}}.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
agent_id: z.string(),
|
|
96
|
+
phone_number_id: z.string(),
|
|
97
|
+
record_calls: z.boolean().optional(),
|
|
98
|
+
recording_public: z.boolean().optional(),
|
|
99
|
+
office_hours: officeHoursNumberSchema.optional(),
|
|
100
|
+
metadata_tags: z.record(z.string(), z.string()).optional(),
|
|
101
|
+
secret_bindings: z
|
|
102
|
+
.record(z.string(), z.string())
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("alias -> existing workspace secret name; 400 if a name doesn't exist"),
|
|
105
|
+
label: z.string().optional(),
|
|
106
|
+
transfer_caller_id_mode: z.enum(["from-transferee", "from-transferor"]).optional(),
|
|
107
|
+
...workspaceParam,
|
|
108
|
+
},
|
|
109
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
110
|
+
}, guard(async (a) => {
|
|
111
|
+
const body = {};
|
|
112
|
+
if (a.record_calls !== undefined)
|
|
113
|
+
body.recordCalls = a.record_calls;
|
|
114
|
+
if (a.recording_public !== undefined)
|
|
115
|
+
body.recordingPublic = a.recording_public;
|
|
116
|
+
if (a.office_hours !== undefined)
|
|
117
|
+
body.officeHours = a.office_hours;
|
|
118
|
+
if (a.metadata_tags !== undefined)
|
|
119
|
+
body.metadataTags = a.metadata_tags;
|
|
120
|
+
if (a.secret_bindings !== undefined)
|
|
121
|
+
body.secretBindings = a.secret_bindings;
|
|
122
|
+
if (a.label !== undefined)
|
|
123
|
+
body.label = a.label;
|
|
124
|
+
if (a.transfer_caller_id_mode !== undefined)
|
|
125
|
+
body.transferCallerIdMode = a.transfer_caller_id_mode;
|
|
126
|
+
const res = await client.request("POST", `/v1/api/agents/${a.agent_id}/bind/${a.phone_number_id}`, { body, workspaceId: a.workspace_id });
|
|
127
|
+
return ok({
|
|
128
|
+
bound: true,
|
|
129
|
+
phoneNumber: res.phoneNumber?.phoneNumber,
|
|
130
|
+
agent: res.agent?.name,
|
|
131
|
+
direction: res.direction,
|
|
132
|
+
});
|
|
133
|
+
}));
|
|
134
|
+
server.registerTool("trillet_release_phone_number", {
|
|
135
|
+
title: "Release phone number from agent",
|
|
136
|
+
description: "Unbind a phone number from an agent (the number stays owned by the workspace, it just stops routing to the agent). Fails if the number is not currently bound to that agent.",
|
|
137
|
+
inputSchema: { agent_id: z.string(), phone_number_id: z.string(), ...workspaceParam },
|
|
138
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
139
|
+
}, guard(async ({ agent_id, phone_number_id, workspace_id }) => {
|
|
140
|
+
const res = await client.request("POST", `/v1/api/agents/${agent_id}/release/${phone_number_id}`, { workspaceId: workspace_id });
|
|
141
|
+
return ok(res);
|
|
142
|
+
}));
|
|
143
|
+
server.registerTool("trillet_get_phone_number_config", {
|
|
144
|
+
title: "Get phone number config",
|
|
145
|
+
description: "Fetch a number's routing config: agent, direction, recording, office hours, tags, bindings.",
|
|
146
|
+
inputSchema: { phone_number_id: z.string(), ...workspaceParam },
|
|
147
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
148
|
+
}, guard(async ({ phone_number_id, workspace_id }) => {
|
|
149
|
+
const res = await client.request("GET", `/v1/api/twilio/phone-numbers/${phone_number_id}/config`, { workspaceId: workspace_id });
|
|
150
|
+
return ok(res);
|
|
151
|
+
}));
|
|
152
|
+
server.registerTool("trillet_update_phone_number_config", {
|
|
153
|
+
title: "Update phone number config",
|
|
154
|
+
description: "Update a number's routing config in place: agent, max call duration, recording, office hours, caller-id mode on transfers, metadata tags, secret bindings, label. Only the fields you send change. Every secret name in secret_bindings must already exist as a workspace secret (400 otherwise).",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
phone_number_id: z.string(),
|
|
157
|
+
agent_id: z.string().optional(),
|
|
158
|
+
max_call_duration: z.number().optional().describe("Seconds"),
|
|
159
|
+
recording_enabled: z.boolean().optional(),
|
|
160
|
+
office_hours: officeHoursNumberSchema.optional(),
|
|
161
|
+
transfer_caller_id_mode: z.enum(["from-transferee", "from-transferor"]).optional(),
|
|
162
|
+
metadata_tags: z.record(z.string(), z.string()).optional(),
|
|
163
|
+
secret_bindings: z
|
|
164
|
+
.record(z.string(), z.string())
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("alias -> existing workspace secret name"),
|
|
167
|
+
label: z.string().optional(),
|
|
168
|
+
...workspaceParam,
|
|
169
|
+
},
|
|
170
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
171
|
+
}, guard(async (a) => {
|
|
172
|
+
const body = {};
|
|
173
|
+
if (a.agent_id !== undefined)
|
|
174
|
+
body.agentId = a.agent_id;
|
|
175
|
+
if (a.max_call_duration !== undefined)
|
|
176
|
+
body.maxCallDuration = a.max_call_duration;
|
|
177
|
+
if (a.recording_enabled !== undefined)
|
|
178
|
+
body.recordingEnabled = a.recording_enabled;
|
|
179
|
+
if (a.office_hours !== undefined)
|
|
180
|
+
body.officeHours = a.office_hours;
|
|
181
|
+
if (a.transfer_caller_id_mode !== undefined)
|
|
182
|
+
body.transferCallerIdMode = a.transfer_caller_id_mode;
|
|
183
|
+
if (a.metadata_tags !== undefined)
|
|
184
|
+
body.metadataTags = a.metadata_tags;
|
|
185
|
+
if (a.secret_bindings !== undefined)
|
|
186
|
+
body.secretBindings = a.secret_bindings;
|
|
187
|
+
if (a.label !== undefined)
|
|
188
|
+
body.label = a.label;
|
|
189
|
+
const res = await client.request("PUT", `/v1/api/twilio/phone-numbers/${a.phone_number_id}/config`, { body, workspaceId: a.workspace_id });
|
|
190
|
+
return ok(res);
|
|
191
|
+
}));
|
|
192
|
+
server.registerTool("trillet_register_external_number", {
|
|
193
|
+
title: "Register BYO / SIP number",
|
|
194
|
+
description: "Register an externally-owned number (bring-your-own Twilio number or a SIP trunk from another carrier, e.g. CommsChannel) so Trillet can route calls on it. provider 'twilio' needs {accountSid, authToken}; provider 'sip' needs trunk credentials {address, username, password, sipType, appendPlus}. After registering, bind it to an agent like any other number. GOTCHA: DTMF keypresses are known to be dropped on some CommsChannel trunks.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
phone_number: z.string().describe("E.164 number being brought over"),
|
|
197
|
+
provider: z.enum(["twilio", "sip"]),
|
|
198
|
+
credentials: z
|
|
199
|
+
.record(z.string(), z.unknown())
|
|
200
|
+
.describe("Provider credentials. twilio: {accountSid, authToken}. sip: {address, username, password, sipType, appendPlus}"),
|
|
201
|
+
confirm: z
|
|
202
|
+
.boolean()
|
|
203
|
+
.describe("Must be true. Only set after the user explicitly approved registering this number."),
|
|
204
|
+
...workspaceParam,
|
|
205
|
+
},
|
|
206
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
207
|
+
}, guard(async ({ phone_number, provider, credentials, confirm, workspace_id }) => {
|
|
208
|
+
if (!confirm) {
|
|
209
|
+
return ok({
|
|
210
|
+
blocked: true,
|
|
211
|
+
message: "Refusing: confirm=false. Registering an external number provisions trunks; ask the user to approve first.",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const workspaceId = workspace_id || (process.env.TRILLET_WORKSPACE_ID || "").trim();
|
|
215
|
+
const res = await client.request("POST", "/v1/api/twilio/register-external-number", {
|
|
216
|
+
body: { phoneNumber: phone_number, provider, credentials, workspaceId },
|
|
217
|
+
workspaceId: workspace_id,
|
|
218
|
+
});
|
|
219
|
+
return ok(res);
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, guard, workspaceParam } from "../helpers.js";
|
|
3
|
+
export function registerSecretTools(server, client) {
|
|
4
|
+
server.registerTool("trillet_list_secrets", {
|
|
5
|
+
title: "List workspace secrets",
|
|
6
|
+
description: "List workspace secrets (names and field names only; values are always masked and can never be read back). Reference syntax: {{_secret.<name>}} reads the api_key field, {{_secret.<name>.<field>}} reads a specific field. Code steps read them as secrets.<name>.<field>.",
|
|
7
|
+
inputSchema: { ...workspaceParam },
|
|
8
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
9
|
+
}, guard(async ({ workspace_id }) => {
|
|
10
|
+
const res = await client.request("GET", "/v2/api/workspaces/secrets", {
|
|
11
|
+
workspaceId: workspace_id,
|
|
12
|
+
});
|
|
13
|
+
return ok(res.secrets ?? res);
|
|
14
|
+
}));
|
|
15
|
+
server.registerTool("trillet_set_secret", {
|
|
16
|
+
title: "Create or update workspace secret",
|
|
17
|
+
description: "Create a workspace secret or merge fields into an existing one (upsert). Name must be slug-safe lowercase (letters, numbers, hyphen, underscore); field keys alphanumeric/underscore; values non-empty strings, encrypted at rest. Store third-party API keys here instead of pasting them into tool headers, then reference {{_secret.<name>}} in in-call API tool headers/urls.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
name: z.string().describe('Slug-safe, e.g. "airtable" or "stripe-test"'),
|
|
20
|
+
values: z
|
|
21
|
+
.record(z.string(), z.string())
|
|
22
|
+
.describe('Fields, e.g. {"api_key": "...", "base_url": "..."}. api_key is the default field.'),
|
|
23
|
+
description: z.string().optional(),
|
|
24
|
+
...workspaceParam,
|
|
25
|
+
},
|
|
26
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
27
|
+
}, guard(async ({ name, values, description, workspace_id }) => {
|
|
28
|
+
const res = await client.request("POST", "/v2/api/workspaces/secrets", {
|
|
29
|
+
body: { name, values, description },
|
|
30
|
+
workspaceId: workspace_id,
|
|
31
|
+
});
|
|
32
|
+
return ok(res);
|
|
33
|
+
}));
|
|
34
|
+
server.registerTool("trillet_delete_secret", {
|
|
35
|
+
title: "Delete workspace secret",
|
|
36
|
+
description: "Delete a workspace secret by name. Any {{_secret.<name>}} references in tools or workflows will resolve to empty strings afterwards.",
|
|
37
|
+
inputSchema: { name: z.string(), ...workspaceParam },
|
|
38
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
39
|
+
}, guard(async ({ name, workspace_id }) => {
|
|
40
|
+
const res = await client.request("DELETE", `/v2/api/workspaces/secrets/${encodeURIComponent(name)}`, {
|
|
41
|
+
workspaceId: workspace_id,
|
|
42
|
+
});
|
|
43
|
+
return ok(res);
|
|
44
|
+
}));
|
|
45
|
+
}
|