@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +178 -80
  3. package/dist/annotations.js +45 -0
  4. package/dist/completions.js +110 -0
  5. package/dist/config.js +55 -29
  6. package/dist/errors.js +56 -15
  7. package/dist/generated/openapi-inputs.js +4324 -1429
  8. package/dist/generated/openapi-outputs.js +21992 -8516
  9. package/dist/generated/route-catalog.js +1480 -0
  10. package/dist/http.js +97 -103
  11. package/dist/index.js +16 -10
  12. package/dist/oauth-resolver.js +59 -37
  13. package/dist/observability.js +12 -4
  14. package/dist/openapi-zod.js +138 -72
  15. package/dist/operations/catalog.js +50 -0
  16. package/dist/operations/groups/analytics.js +122 -0
  17. package/dist/operations/groups/bookings.js +189 -0
  18. package/dist/operations/groups/broadcasts.js +106 -0
  19. package/dist/operations/groups/cadences.js +14 -0
  20. package/dist/operations/groups/callEvents.js +131 -0
  21. package/dist/operations/groups/campaigns.js +132 -0
  22. package/dist/operations/groups/capabilities.js +8 -0
  23. package/dist/operations/groups/coach.js +43 -0
  24. package/dist/operations/groups/conversations.js +48 -0
  25. package/dist/operations/groups/insights.js +33 -0
  26. package/dist/operations/groups/integrations.js +80 -0
  27. package/dist/operations/groups/knowledge.js +34 -0
  28. package/dist/operations/groups/leads.js +101 -0
  29. package/dist/operations/groups/learning.js +24 -0
  30. package/dist/operations/groups/memory.js +27 -0
  31. package/dist/operations/groups/reports.js +14 -0
  32. package/dist/operations/groups/reviewQueue.js +57 -0
  33. package/dist/operations/groups/safety.js +8 -0
  34. package/dist/operations/groups/usage.js +13 -0
  35. package/dist/operations/groups/voice.js +14 -0
  36. package/dist/operations/groups/webhooks.js +76 -0
  37. package/dist/operations/manifest.js +40 -0
  38. package/dist/operations/registry.js +331 -0
  39. package/dist/operations.js +11 -780
  40. package/dist/optional-field.js +12 -0
  41. package/dist/page-shape.js +62 -0
  42. package/dist/pagination.js +165 -0
  43. package/dist/prompts.js +89 -8
  44. package/dist/provenance.js +18 -2
  45. package/dist/remote.js +80 -49
  46. package/dist/resilience.js +127 -0
  47. package/dist/resources.js +17 -2
  48. package/dist/scopes.js +154 -0
  49. package/dist/serialization.js +36 -0
  50. package/dist/server.js +258 -316
  51. package/dist/task-tools/analytics.js +168 -0
  52. package/dist/task-tools/bookings.js +157 -0
  53. package/dist/task-tools/cadences.js +65 -0
  54. package/dist/task-tools/calls.js +202 -0
  55. package/dist/task-tools/inbox.js +120 -0
  56. package/dist/task-tools/leads.js +123 -0
  57. package/dist/task-tools/runtime.js +92 -0
  58. package/dist/task-tools.js +21 -0
  59. package/dist/tool-cache.js +39 -0
  60. package/dist/tool.js +228 -0
  61. package/dist/tsconfig.tsbuildinfo +1 -1
  62. package/dist/version.js +14 -0
  63. 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
+ }