@setluca/mcp 0.1.1 → 0.2.1
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/CHANGELOG.md +110 -0
- package/README.md +178 -80
- package/dist/annotations.js +45 -0
- package/dist/completions.js +110 -0
- package/dist/config.js +55 -29
- package/dist/errors.js +56 -15
- package/dist/generated/openapi-inputs.js +4324 -1429
- package/dist/generated/openapi-outputs.js +21992 -8516
- package/dist/generated/route-catalog.js +1480 -0
- package/dist/http.js +97 -103
- package/dist/index.js +16 -10
- package/dist/oauth-resolver.js +59 -37
- package/dist/observability.js +12 -4
- package/dist/openapi-zod.js +138 -72
- package/dist/operations/catalog.js +50 -0
- package/dist/operations/groups/analytics.js +122 -0
- package/dist/operations/groups/bookings.js +189 -0
- package/dist/operations/groups/broadcasts.js +106 -0
- package/dist/operations/groups/cadences.js +14 -0
- package/dist/operations/groups/callEvents.js +131 -0
- package/dist/operations/groups/campaigns.js +132 -0
- package/dist/operations/groups/capabilities.js +8 -0
- package/dist/operations/groups/coach.js +43 -0
- package/dist/operations/groups/conversations.js +48 -0
- package/dist/operations/groups/insights.js +33 -0
- package/dist/operations/groups/integrations.js +80 -0
- package/dist/operations/groups/knowledge.js +34 -0
- package/dist/operations/groups/leads.js +101 -0
- package/dist/operations/groups/learning.js +24 -0
- package/dist/operations/groups/memory.js +27 -0
- package/dist/operations/groups/reports.js +14 -0
- package/dist/operations/groups/reviewQueue.js +57 -0
- package/dist/operations/groups/safety.js +8 -0
- package/dist/operations/groups/usage.js +13 -0
- package/dist/operations/groups/voice.js +14 -0
- package/dist/operations/groups/webhooks.js +76 -0
- package/dist/operations/manifest.js +40 -0
- package/dist/operations/registry.js +331 -0
- package/dist/operations.js +11 -780
- package/dist/optional-field.js +12 -0
- package/dist/page-shape.js +62 -0
- package/dist/pagination.js +165 -0
- package/dist/prompts.js +89 -8
- package/dist/provenance.js +18 -2
- package/dist/remote.js +80 -49
- package/dist/resilience.js +127 -0
- package/dist/resources.js +17 -2
- package/dist/scopes.js +154 -0
- package/dist/serialization.js +36 -0
- package/dist/server.js +258 -316
- package/dist/task-tools/analytics.js +168 -0
- package/dist/task-tools/bookings.js +157 -0
- package/dist/task-tools/cadences.js +65 -0
- package/dist/task-tools/calls.js +202 -0
- package/dist/task-tools/inbox.js +120 -0
- package/dist/task-tools/leads.js +123 -0
- package/dist/task-tools/runtime.js +92 -0
- package/dist/task-tools.js +21 -0
- package/dist/tool-cache.js +39 -0
- package/dist/tool.js +228 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.js +14 -0
- package/package.json +38 -34
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { OPENAPI_INPUTS } from "../generated/openapi-inputs.js";
|
|
3
|
+
import { OPENAPI_OUTPUTS } from "../generated/openapi-outputs.js";
|
|
4
|
+
import { ROUTE_CATALOG } from "../generated/route-catalog.js";
|
|
5
|
+
import { openApiToolInputFields, openApiToolOutputFields, } from "../openapi-zod.js";
|
|
6
|
+
import { optionalField } from "../optional-field.js";
|
|
7
|
+
import { pageShapeFor } from "../page-shape.js";
|
|
8
|
+
export const LUCA_OPERATION_GROUPS = {
|
|
9
|
+
capabilities: {
|
|
10
|
+
title: "Capabilities",
|
|
11
|
+
description: "Public API capabilities and boundary discovery.",
|
|
12
|
+
},
|
|
13
|
+
leads: {
|
|
14
|
+
title: "Leads",
|
|
15
|
+
description: "Lead identity, consent, notes, custom fields, and imports.",
|
|
16
|
+
},
|
|
17
|
+
conversations: {
|
|
18
|
+
title: "Conversations",
|
|
19
|
+
description: "Conversation search and message context.",
|
|
20
|
+
},
|
|
21
|
+
reviewQueue: {
|
|
22
|
+
title: "Review Queue",
|
|
23
|
+
description: "Human review queue visibility.",
|
|
24
|
+
},
|
|
25
|
+
bookings: {
|
|
26
|
+
title: "Bookings",
|
|
27
|
+
description: "Booking creation, scheduling, and cancellation.",
|
|
28
|
+
},
|
|
29
|
+
callEvents: {
|
|
30
|
+
title: "Call Events",
|
|
31
|
+
description: "What happened on a call: outcome, coach feedback, summary, and CRM push.",
|
|
32
|
+
},
|
|
33
|
+
campaigns: {
|
|
34
|
+
title: "Campaigns",
|
|
35
|
+
description: "Campaign drafts, enrollment, analytics, comments, and simulations.",
|
|
36
|
+
},
|
|
37
|
+
broadcasts: {
|
|
38
|
+
title: "Broadcasts",
|
|
39
|
+
description: "Broadcast draft, approval, launch, pause, retry, and cancel flows.",
|
|
40
|
+
},
|
|
41
|
+
webhooks: {
|
|
42
|
+
title: "Webhooks",
|
|
43
|
+
description: "Webhook subscriptions, deliveries, events, signatures, and replays.",
|
|
44
|
+
},
|
|
45
|
+
integrations: {
|
|
46
|
+
title: "Integrations",
|
|
47
|
+
description: "CRM connections, OAuth, mappings, sync runs, and event logs.",
|
|
48
|
+
},
|
|
49
|
+
voice: {
|
|
50
|
+
title: "Voice",
|
|
51
|
+
description: "The voice fingerprint and the corpus behind it, read-only. Corpus text is blanked for a redacted-tier key.",
|
|
52
|
+
},
|
|
53
|
+
coach: {
|
|
54
|
+
title: "Coach",
|
|
55
|
+
description: "The coach's own configuration: profile, niche and offer, objection playbook, post-call preferences. Read-only.",
|
|
56
|
+
},
|
|
57
|
+
usage: {
|
|
58
|
+
title: "Usage",
|
|
59
|
+
description: "Message-pool and model-spend counters for the current plan.",
|
|
60
|
+
},
|
|
61
|
+
safety: {
|
|
62
|
+
title: "Account Safety",
|
|
63
|
+
description: "Per-channel account health, send pacing, and the human-approval window.",
|
|
64
|
+
},
|
|
65
|
+
insights: {
|
|
66
|
+
title: "Insights",
|
|
67
|
+
description: "The proactive-insights feed and the acknowledgements that clear it.",
|
|
68
|
+
},
|
|
69
|
+
learning: {
|
|
70
|
+
title: "Teach Luca",
|
|
71
|
+
description: "The profile questions Luca still owes an answer to, and answering them.",
|
|
72
|
+
},
|
|
73
|
+
knowledge: {
|
|
74
|
+
title: "Knowledge",
|
|
75
|
+
description: "Profile suggestions extracted from the coach's own knowledge sources.",
|
|
76
|
+
},
|
|
77
|
+
memory: {
|
|
78
|
+
title: "Memory",
|
|
79
|
+
description: "What Luca remembers about a lead or the business, and how to correct it.",
|
|
80
|
+
},
|
|
81
|
+
cadences: {
|
|
82
|
+
title: "Cadences",
|
|
83
|
+
description: "Silent-lead rescue cadence control.",
|
|
84
|
+
},
|
|
85
|
+
reports: {
|
|
86
|
+
title: "Reports",
|
|
87
|
+
description: "Digest reports over the coach's workspace.",
|
|
88
|
+
},
|
|
89
|
+
analytics: {
|
|
90
|
+
title: "Analytics",
|
|
91
|
+
description: "Business intelligence, funnel analytics, revenue attribution, and coach performance metrics.",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
function requiredScopeFor(input) {
|
|
95
|
+
let capability = "draft";
|
|
96
|
+
if (input.method === "GET") {
|
|
97
|
+
capability = "read";
|
|
98
|
+
}
|
|
99
|
+
else if (input.confirmRequired) {
|
|
100
|
+
// Real-world side effect (sends, enrollments, replays) = operating the
|
|
101
|
+
// live queue.
|
|
102
|
+
capability = "queue_ops";
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
capability,
|
|
106
|
+
dataSensitivity: input.untrustedContent ? "full_content" : "redacted",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Operation groups whose responses carry text authored by leads or other
|
|
111
|
+
* external users (conversation messages, lead-provided profile fields, the
|
|
112
|
+
* review queue's inbound context). Output from these tools is wrapped with an
|
|
113
|
+
* untrusted-content boundary so a downstream agent treats it as data, never as
|
|
114
|
+
* instructions (prompt-injection defense).
|
|
115
|
+
*/
|
|
116
|
+
const UNTRUSTED_CONTENT_GROUPS = new Set([
|
|
117
|
+
"callEvents",
|
|
118
|
+
"conversations",
|
|
119
|
+
"leads",
|
|
120
|
+
// A memory is a fact Luca extracted from a lead's message, and the Teach
|
|
121
|
+
// Luca panel shows a lead DM the coach pasted in. Both read back as the
|
|
122
|
+
// lead's words even though neither group is named after a lead.
|
|
123
|
+
"learning",
|
|
124
|
+
"memory",
|
|
125
|
+
"reviewQueue",
|
|
126
|
+
]);
|
|
127
|
+
/** Server-side bounded auto-pagination guard for list tools. */
|
|
128
|
+
export const DEFAULT_MAX_PAGES = 10;
|
|
129
|
+
const MAX_PAGES_LIMIT = 50;
|
|
130
|
+
const OPENAPI_INPUTS_BY_ROUTE = new Map(Object.entries(OPENAPI_INPUTS));
|
|
131
|
+
const OPENAPI_OUTPUTS_BY_ROUTE = new Map(Object.entries(OPENAPI_OUTPUTS));
|
|
132
|
+
export const WORKSPACE_INPUT_FIELDS = {
|
|
133
|
+
workspaceId: z
|
|
134
|
+
.uuid()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Optional Luca workspace id override."),
|
|
137
|
+
workspaceSlug: z
|
|
138
|
+
.string()
|
|
139
|
+
.min(1)
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("Optional Luca workspace slug override."),
|
|
142
|
+
};
|
|
143
|
+
const idempotencyInput = {
|
|
144
|
+
idempotencyKey: z
|
|
145
|
+
.string()
|
|
146
|
+
.min(1)
|
|
147
|
+
.optional()
|
|
148
|
+
.describe("Stable key for safe retries. If omitted, the MCP server generates a unique key for this call."),
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The confirmation gate's input field, shared by the generated operation tools
|
|
152
|
+
* and the hand-written task tools. One wording covers both because a client
|
|
153
|
+
* sees one tool list: the effects named here are the union of what either kind
|
|
154
|
+
* can do, not a guess at which one the reader is holding.
|
|
155
|
+
*/
|
|
156
|
+
export const confirmInput = {
|
|
157
|
+
confirm: z
|
|
158
|
+
.boolean()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("This tool has a real-world side effect: it messages a real lead, starts outbound touches, or re-triggers downstream automations. Pass confirm: true to proceed; the call is rejected without it."),
|
|
161
|
+
};
|
|
162
|
+
const paginationInput = {
|
|
163
|
+
maxPages: z
|
|
164
|
+
.number()
|
|
165
|
+
.int()
|
|
166
|
+
.min(1)
|
|
167
|
+
.max(MAX_PAGES_LIMIT)
|
|
168
|
+
.optional()
|
|
169
|
+
.describe(`Max pages the server auto-fetches and merges (default ${DEFAULT_MAX_PAGES}, hard cap ${MAX_PAGES_LIMIT}). If more remain, the result is marked truncated with a nextCursor to continue.`),
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Fold the fields every operation tool shares, meaning query, body, idempotency
|
|
173
|
+
* key, and workspace override, into a request fields, omitting each when absent
|
|
174
|
+
* so the outgoing request never carries `undefined` keys. Path params are the one
|
|
175
|
+
* per-operation piece and are assembled separately by {@link pathParams}.
|
|
176
|
+
*/
|
|
177
|
+
function commonInput(input) {
|
|
178
|
+
return {
|
|
179
|
+
...optionalField("query", input.query),
|
|
180
|
+
...optionalField("body", input.body),
|
|
181
|
+
...optionalField("idempotencyKey", input.idempotencyKey),
|
|
182
|
+
workspace: {
|
|
183
|
+
...optionalField("workspaceId", input.workspaceId),
|
|
184
|
+
...optionalField("workspaceSlug", input.workspaceSlug),
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function pathParams(input, path) {
|
|
189
|
+
const params = {};
|
|
190
|
+
for (const [, key] of path.matchAll(/\{([^}]+)\}/g)) {
|
|
191
|
+
// The capture group requires at least one character, so a match always
|
|
192
|
+
// carries a non-empty key. This guard exists to satisfy the compiler's
|
|
193
|
+
// view of `RegExpMatchArray`, and no path string can reach it.
|
|
194
|
+
// Stryker disable next-line ConditionalExpression,BlockStatement: unreachable, the capture group cannot match empty
|
|
195
|
+
if (!key) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const value = z.string().safeParse(input[key]);
|
|
199
|
+
if (value.success) {
|
|
200
|
+
params[key] = value.data;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return params;
|
|
204
|
+
}
|
|
205
|
+
function inputSchemaFor(input) {
|
|
206
|
+
const fields = openApiToolInputFields({
|
|
207
|
+
spec: OPENAPI_INPUTS_BY_ROUTE.get(`${input.method} ${input.path}`),
|
|
208
|
+
workspaceInput: WORKSPACE_INPUT_FIELDS,
|
|
209
|
+
idempotencyInput,
|
|
210
|
+
idempotencyRequired: input.idempotencyRequired,
|
|
211
|
+
});
|
|
212
|
+
if (input.confirmRequired) {
|
|
213
|
+
Object.assign(fields, confirmInput);
|
|
214
|
+
}
|
|
215
|
+
if (input.paginates) {
|
|
216
|
+
Object.assign(fields, paginationInput);
|
|
217
|
+
}
|
|
218
|
+
return fields;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Build an operation's output schema and, if it declares `redactedFields`
|
|
222
|
+
* (see the `op()` parameter doc), attach an
|
|
223
|
+
* `x-luca-redacted-fields` marker to `result` so an MCP client can tell those
|
|
224
|
+
* fields may arrive blanked for a `redacted`-tier key. `.meta()` is metadata
|
|
225
|
+
* only. It never affects `.parse()` or runtime validation, and it is defined
|
|
226
|
+
* on the classic `z.ZodType` interface, not the core `$ZodType` that
|
|
227
|
+
* `ZodFields` values are typed as. The cast is safe because every value
|
|
228
|
+
* here is built by `schemaToZod()` in `openapi-zod.ts`, which only ever
|
|
229
|
+
* constructs classic zod instances.
|
|
230
|
+
*/
|
|
231
|
+
export function outputSchemaFor(input) {
|
|
232
|
+
const fields = openApiToolOutputFields({
|
|
233
|
+
body: OPENAPI_OUTPUTS_BY_ROUTE.get(`${input.method} ${input.path}`),
|
|
234
|
+
untrustedContent: input.untrustedContent,
|
|
235
|
+
});
|
|
236
|
+
// This guard also runs while `LUCA_OPERATIONS` is built at module
|
|
237
|
+
// initialization. Forcing either half of it makes the second operand read
|
|
238
|
+
// `.length` off an absent `redactedFields`, which throws during import: the
|
|
239
|
+
// test file then fails to collect and reports zero tests, so there is no
|
|
240
|
+
// failing test for the mutation runner to attribute the kill to and it
|
|
241
|
+
// registers as a survivor. The branch itself is real and covered: the absent,
|
|
242
|
+
// empty, and populated cases all assert on the marker.
|
|
243
|
+
// Stryker disable next-line LogicalOperator,BooleanLiteral: crashes at import, leaving no test to attribute the kill to
|
|
244
|
+
if (!input.redactedFields || input.redactedFields.length === 0) {
|
|
245
|
+
return fields;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
...fields,
|
|
249
|
+
// openApiToolOutputFields always sets `result`; ZodRawShape's index
|
|
250
|
+
// signature cannot express that invariant.
|
|
251
|
+
result: z.any().pipe(fields.result).meta({
|
|
252
|
+
"x-luca-redacted-fields": input.redactedFields,
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The group is the id's prefix before the first dot. `op()`'s `id` parameter
|
|
258
|
+
* is typed as {@link LucaOperationId}, so a prefix that doesn't name a real
|
|
259
|
+
* group is a compile-time error at the `op({...})` call site in
|
|
260
|
+
* `catalog.ts`, so a typo'd id never reaches this function. The split below
|
|
261
|
+
* only recovers, at runtime, the value TypeScript already proved is one of
|
|
262
|
+
* `LucaOperationGroup`.
|
|
263
|
+
*/
|
|
264
|
+
function operationGroupForId(id) {
|
|
265
|
+
// SAFETY: `id`'s type is `${LucaOperationGroup}.${string}`, so splitting on
|
|
266
|
+
// the first "." always yields a valid group as the first element.
|
|
267
|
+
const [group] = id.split(".");
|
|
268
|
+
return group;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Declare one 1:1 operation tool. The route's id says which allowlisted route
|
|
272
|
+
* it is; {@link ROUTE_CATALOG} answers with the method, path, scopes, and
|
|
273
|
+
* idempotency rule the API grants it. `op()` derives the rest uniformly: the
|
|
274
|
+
* `luca_*` tool name, the group that {@link operationGroupForId} reads off the
|
|
275
|
+
* id prefix, the generated input and output schemas
|
|
276
|
+
* ({@link inputSchemaFor}), the required scope tier ({@link requiredScopeFor}),
|
|
277
|
+
* the untrusted-content boundary, and a `buildRequest` that maps tool input to
|
|
278
|
+
* an HTTP request. What stays here is what only the tool surface knows: the
|
|
279
|
+
* title, the agent-facing description, the confirmation gate, and which fields
|
|
280
|
+
* can come back blanked. Task tools (composed, intent-level) are declared
|
|
281
|
+
* separately in `task-tools.ts`.
|
|
282
|
+
*/
|
|
283
|
+
export function op(input) {
|
|
284
|
+
const route = ROUTE_CATALOG[input.id];
|
|
285
|
+
// SAFETY: `ROUTE_CATALOG` is generated from the allowlist and its methods are
|
|
286
|
+
// the ones the public API serves; the snapshot carries them as plain strings.
|
|
287
|
+
const method = route.method;
|
|
288
|
+
const { path, scopes, idempotencyRequired } = route;
|
|
289
|
+
const confirmRequired = input.confirmRequired ?? false;
|
|
290
|
+
const pageShape = pageShapeFor(method, path);
|
|
291
|
+
const group = operationGroupForId(input.id);
|
|
292
|
+
const untrustedContent = input.untrustedContent ??
|
|
293
|
+
(method === "GET" && UNTRUSTED_CONTENT_GROUPS.has(group));
|
|
294
|
+
const operation = {
|
|
295
|
+
id: input.id,
|
|
296
|
+
group,
|
|
297
|
+
toolName: `luca_${input.id.replaceAll(".", "_")}`,
|
|
298
|
+
title: input.title,
|
|
299
|
+
method,
|
|
300
|
+
path,
|
|
301
|
+
scopes,
|
|
302
|
+
idempotencyRequired,
|
|
303
|
+
confirmRequired,
|
|
304
|
+
...optionalField("pageShape", pageShape),
|
|
305
|
+
untrustedContent,
|
|
306
|
+
requiredScope: requiredScopeFor({
|
|
307
|
+
method,
|
|
308
|
+
confirmRequired,
|
|
309
|
+
untrustedContent,
|
|
310
|
+
}),
|
|
311
|
+
description: input.description,
|
|
312
|
+
inputSchema: inputSchemaFor({
|
|
313
|
+
method,
|
|
314
|
+
path,
|
|
315
|
+
idempotencyRequired,
|
|
316
|
+
confirmRequired,
|
|
317
|
+
paginates: pageShape !== undefined,
|
|
318
|
+
}),
|
|
319
|
+
outputSchema: outputSchemaFor({
|
|
320
|
+
method,
|
|
321
|
+
path,
|
|
322
|
+
untrustedContent,
|
|
323
|
+
...optionalField("redactedFields", input.redactedFields),
|
|
324
|
+
}),
|
|
325
|
+
buildRequest: (toolInput) => ({
|
|
326
|
+
pathParams: pathParams(toolInput, path),
|
|
327
|
+
...commonInput(toolInput),
|
|
328
|
+
}),
|
|
329
|
+
};
|
|
330
|
+
return operation;
|
|
331
|
+
}
|