@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
@@ -1,780 +1,11 @@
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 { openApiToolInputShape, openApiToolOutputShape, } from "./openapi-zod.js";
5
- const LUCA_OPERATION_GROUPS = {
6
- capabilities: {
7
- title: "Capabilities",
8
- description: "Public API capabilities and boundary discovery.",
9
- },
10
- leads: {
11
- title: "Leads",
12
- description: "Lead identity, consent, notes, custom fields, and imports.",
13
- },
14
- conversations: {
15
- title: "Conversations",
16
- description: "Conversation search and message context.",
17
- },
18
- reviewQueue: {
19
- title: "Review Queue",
20
- description: "Human review queue visibility.",
21
- },
22
- bookings: {
23
- title: "Bookings",
24
- description: "Booking creation, scheduling, and cancellation.",
25
- },
26
- campaigns: {
27
- title: "Campaigns",
28
- description: "Campaign drafts, enrollment, analytics, comments, and simulations.",
29
- },
30
- broadcasts: {
31
- title: "Broadcasts",
32
- description: "Broadcast draft, approval, launch, pause, retry, and cancel flows.",
33
- },
34
- webhooks: {
35
- title: "Webhooks",
36
- description: "Webhook subscriptions, deliveries, signatures, and replays.",
37
- },
38
- integrations: {
39
- title: "Integrations",
40
- description: "CRM connections, OAuth, mappings, sync runs, and event logs.",
41
- },
42
- voice: {
43
- title: "Voice",
44
- description: "Read-only voice-fingerprint summary.",
45
- },
46
- };
47
- function requiredScopeFor(input) {
48
- let capability = "draft";
49
- if (input.method === "GET") {
50
- capability = "read";
51
- }
52
- else if (input.confirmRequired) {
53
- // Real-world side effect (sends, enrollments, replays) = operating the
54
- // live queue.
55
- capability = "queue_ops";
56
- }
57
- return {
58
- capability,
59
- dataSensitivity: input.untrustedContent ? "full_content" : "redacted",
60
- };
61
- }
62
- /**
63
- * Operation groups whose responses carry text authored by leads or other
64
- * external users (conversation messages, lead-provided profile fields, the
65
- * review queue's inbound context). Output from these tools is wrapped with an
66
- * untrusted-content boundary so a downstream agent treats it as data, never as
67
- * instructions (prompt-injection defense).
68
- */
69
- const UNTRUSTED_CONTENT_GROUPS = new Set([
70
- "conversations",
71
- "leads",
72
- "reviewQueue",
73
- ]);
74
- /** Server-side bounded auto-pagination guard for list tools. */
75
- export const DEFAULT_MAX_PAGES = 10;
76
- const MAX_PAGES_LIMIT = 50;
77
- const workspaceInput = {
78
- workspaceId: z
79
- .string()
80
- .uuid()
81
- .optional()
82
- .describe("Optional Luca workspace id override."),
83
- workspaceSlug: z
84
- .string()
85
- .min(1)
86
- .optional()
87
- .describe("Optional Luca workspace slug override."),
88
- };
89
- const idempotencyInput = {
90
- idempotencyKey: z
91
- .string()
92
- .min(1)
93
- .optional()
94
- .describe("Stable key for safe retries. If omitted, the MCP server generates a unique key for this call."),
95
- };
96
- const confirmInput = {
97
- confirm: z
98
- .boolean()
99
- .optional()
100
- .describe("This tool has a real-world side effect (sends messages or re-triggers downstream automations). Pass confirm: true to proceed; the call is rejected without it."),
101
- };
102
- const paginationInput = {
103
- maxPages: z
104
- .number()
105
- .int()
106
- .min(1)
107
- .max(MAX_PAGES_LIMIT)
108
- .optional()
109
- .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.`),
110
- };
111
- function commonInput(input) {
112
- return {
113
- ...(input.query ? { query: input.query } : {}),
114
- ...(input.body ? { body: input.body } : {}),
115
- ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
116
- workspace: {
117
- ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
118
- ...(input.workspaceSlug ? { workspaceSlug: input.workspaceSlug } : {}),
119
- },
120
- };
121
- }
122
- function pathParams(input, path) {
123
- const params = {};
124
- for (const [, key] of path.matchAll(/\{([^}]+)\}/g)) {
125
- if (!key) {
126
- continue;
127
- }
128
- const value = input[key];
129
- if (typeof value === "string") {
130
- params[key] = value;
131
- }
132
- }
133
- return params;
134
- }
135
- function inputSchemaFor(input) {
136
- const shape = openApiToolInputShape({
137
- spec: OPENAPI_INPUTS[`${input.method} ${input.path}`],
138
- workspaceInput,
139
- idempotencyInput,
140
- idempotencyRequired: input.idempotencyRequired,
141
- });
142
- if (input.confirmRequired) {
143
- Object.assign(shape, confirmInput);
144
- }
145
- if (input.paginates) {
146
- Object.assign(shape, paginationInput);
147
- }
148
- return shape;
149
- }
150
- function operationGroupForId(id) {
151
- const [prefix] = id.split(".");
152
- if (prefix && prefix in LUCA_OPERATION_GROUPS) {
153
- return prefix;
154
- }
155
- throw new Error(`Unknown Luca operation group for operation id: ${id}`);
156
- }
157
- function op(input) {
158
- const idempotencyRequired = input.idempotencyRequired ?? false;
159
- const confirmRequired = input.confirmRequired ?? false;
160
- const paginates = input.method === "GET" && input.id.endsWith(".list");
161
- const group = operationGroupForId(input.id);
162
- const untrustedContent = input.untrustedContent ??
163
- (input.method === "GET" && UNTRUSTED_CONTENT_GROUPS.has(group));
164
- const operation = {
165
- id: input.id,
166
- group,
167
- toolName: `luca_${input.id.replaceAll(".", "_")}`,
168
- title: `Luca ${input.id}`,
169
- method: input.method,
170
- path: input.path,
171
- scopes: input.scopes,
172
- idempotencyRequired,
173
- confirmRequired,
174
- paginates,
175
- untrustedContent,
176
- requiredScope: requiredScopeFor({
177
- method: input.method,
178
- confirmRequired,
179
- untrustedContent,
180
- }),
181
- description: input.description,
182
- inputSchema: inputSchemaFor({
183
- method: input.method,
184
- path: input.path,
185
- idempotencyRequired,
186
- confirmRequired,
187
- paginates,
188
- }),
189
- outputSchema: openApiToolOutputShape({
190
- body: OPENAPI_OUTPUTS[`${input.method} ${input.path}`],
191
- untrustedContent,
192
- }),
193
- buildRequest: (toolInput) => ({
194
- pathParams: pathParams(toolInput, input.path),
195
- ...commonInput(toolInput),
196
- }),
197
- };
198
- return operation;
199
- }
200
- export const LUCA_OPERATIONS = [
201
- op({
202
- id: "capabilities.get",
203
- method: "GET",
204
- path: "/api/capabilities",
205
- scopes: [
206
- "leads:read",
207
- "conversations:read",
208
- "bookings:read",
209
- "campaigns:read",
210
- "broadcasts:read",
211
- "webhooks:read",
212
- "integrations:read",
213
- ],
214
- description: "Describe Luca's public API capabilities, scopes, and boundaries.",
215
- }),
216
- op({
217
- id: "leads.list",
218
- method: "GET",
219
- path: "/api/leads",
220
- scopes: ["leads:read"],
221
- description: "List leads. Query supports limit, cursor, channel, and q according to Luca OpenAPI.",
222
- }),
223
- op({
224
- id: "leads.get",
225
- method: "GET",
226
- path: "/api/leads/{id}",
227
- scopes: ["leads:read"],
228
- description: "Get a lead's full timeline and notes.",
229
- }),
230
- op({
231
- id: "leads.explain",
232
- method: "GET",
233
- path: "/api/leads/{id}/explain",
234
- scopes: ["leads:read"],
235
- description: "Explain why a lead qualified: the qualifier's reasoning, signals, and confidence from its most recent drafted turn.",
236
- }),
237
- op({
238
- id: "leads.create",
239
- method: "POST",
240
- path: "/api/leads",
241
- scopes: ["leads:write"],
242
- idempotencyRequired: true,
243
- description: "Create or upsert a lead identity. Body requires channel and externalUserId.",
244
- }),
245
- op({
246
- id: "leads.update",
247
- method: "PATCH",
248
- path: "/api/leads/{id}",
249
- scopes: ["leads:write"],
250
- idempotencyRequired: true,
251
- description: "Update a lead's display name, stage, score, tags, or custom fields.",
252
- }),
253
- op({
254
- id: "leads.fieldDefinitions.list",
255
- method: "GET",
256
- path: "/api/leads/field-definitions",
257
- scopes: ["leads:read"],
258
- description: "List custom lead field definitions.",
259
- }),
260
- op({
261
- id: "leads.fieldDefinitions.upsert",
262
- method: "POST",
263
- path: "/api/leads/field-definitions",
264
- scopes: ["leads:write"],
265
- idempotencyRequired: true,
266
- description: "Create or update a custom lead field definition.",
267
- }),
268
- op({
269
- id: "leads.importPreview",
270
- method: "POST",
271
- path: "/api/leads/imports/preview",
272
- scopes: ["leads:read"],
273
- description: "Preview a mapped CSV lead import before applying it.",
274
- }),
275
- op({
276
- id: "leads.importApply",
277
- method: "POST",
278
- path: "/api/leads/imports/apply",
279
- scopes: ["leads:write"],
280
- idempotencyRequired: true,
281
- description: "Apply a mapped lead import with consent evidence.",
282
- }),
283
- op({
284
- id: "leads.identities.attach",
285
- method: "POST",
286
- path: "/api/leads/{id}/identities",
287
- scopes: ["leads:write"],
288
- idempotencyRequired: true,
289
- description: "Attach a verified or claimed identity to a lead.",
290
- }),
291
- op({
292
- id: "leads.consents.grant",
293
- method: "POST",
294
- path: "/api/leads/{id}/consents",
295
- scopes: ["leads:write"],
296
- idempotencyRequired: true,
297
- description: "Grant explicit lead consent for a channel purpose.",
298
- }),
299
- op({
300
- id: "leads.consents.revoke",
301
- method: "POST",
302
- path: "/api/leads/{id}/consents/revoke",
303
- scopes: ["leads:write"],
304
- idempotencyRequired: true,
305
- description: "Revoke explicit lead consent for a channel purpose.",
306
- }),
307
- op({
308
- id: "leads.notes.add",
309
- method: "POST",
310
- path: "/api/leads/{id}/notes",
311
- scopes: ["leads:write"],
312
- idempotencyRequired: true,
313
- description: "Append a coach note to a lead. Body requires body.",
314
- }),
315
- op({
316
- id: "conversations.list",
317
- method: "GET",
318
- path: "/api/conversations",
319
- scopes: ["conversations:read"],
320
- description: "List recent conversations. Query supports limit, cursor, channel, and q.",
321
- }),
322
- op({
323
- id: "conversations.get",
324
- method: "GET",
325
- path: "/api/conversations/{id}",
326
- scopes: ["conversations:read"],
327
- description: "Get a conversation with messages and lead context.",
328
- }),
329
- op({
330
- id: "reviewQueue.list",
331
- method: "GET",
332
- path: "/api/review-queue",
333
- scopes: ["review_queue:read"],
334
- description: "List review-queue items when the API key has review queue access.",
335
- }),
336
- op({
337
- id: "reviewQueue.explain",
338
- method: "GET",
339
- path: "/api/review-queue/{id}/explain",
340
- scopes: ["review_queue:read"],
341
- description: "Explain the signals behind a drafted reply: its voice-match score, any critic findings, and the qualification that motivated it.",
342
- }),
343
- op({
344
- id: "bookings.list",
345
- method: "GET",
346
- path: "/api/bookings",
347
- scopes: ["bookings:read"],
348
- description: "List bookings. Query supports limit, cursor, status, and upcoming.",
349
- }),
350
- op({
351
- id: "bookings.create",
352
- method: "POST",
353
- path: "/api/bookings",
354
- scopes: ["bookings:write"],
355
- idempotencyRequired: true,
356
- description: "Create a provider-neutral booking record. Body requires leadId and slotAt.",
357
- }),
358
- op({
359
- id: "bookings.update",
360
- method: "PATCH",
361
- path: "/api/bookings/{id}",
362
- scopes: ["bookings:write"],
363
- idempotencyRequired: true,
364
- description: "Reschedule or update booking status.",
365
- }),
366
- op({
367
- id: "bookings.cancel",
368
- method: "DELETE",
369
- path: "/api/bookings/{id}",
370
- scopes: ["bookings:write"],
371
- idempotencyRequired: true,
372
- description: "Cancel a booking.",
373
- }),
374
- op({
375
- id: "campaigns.list",
376
- method: "GET",
377
- path: "/api/campaigns",
378
- scopes: ["campaigns:read"],
379
- description: "List campaigns. Query supports limit, cursor, and status.",
380
- }),
381
- op({
382
- id: "campaigns.get",
383
- method: "GET",
384
- path: "/api/campaigns/{id}",
385
- scopes: ["campaigns:read"],
386
- description: "Get campaign draft and latest published version.",
387
- }),
388
- op({
389
- id: "campaigns.create",
390
- method: "POST",
391
- path: "/api/campaigns",
392
- scopes: ["campaigns:write"],
393
- idempotencyRequired: true,
394
- description: "Create a campaign draft. Body requires name and may include graph.",
395
- }),
396
- op({
397
- id: "campaigns.generate",
398
- method: "POST",
399
- path: "/api/campaigns/generate",
400
- scopes: ["campaigns:write"],
401
- idempotencyRequired: true,
402
- description: "Generate a safe AI campaign plan without creating or publishing it.",
403
- }),
404
- op({
405
- id: "campaigns.draft.update",
406
- method: "PATCH",
407
- path: "/api/campaigns/{id}/draft",
408
- scopes: ["campaigns:write"],
409
- description: "Autosave a campaign draft.",
410
- // The route requires an idempotency key; without this the tool never sends
411
- // one and every call is rejected (the dead-tool the drift guard now flags).
412
- idempotencyRequired: true,
413
- }),
414
- op({
415
- id: "campaigns.enrollments.list",
416
- method: "GET",
417
- path: "/api/campaigns/{id}/enrollments",
418
- scopes: ["campaigns:read"],
419
- description: "List campaign enrollments.",
420
- }),
421
- op({
422
- id: "campaigns.enroll",
423
- method: "POST",
424
- path: "/api/campaigns/{id}/enrollments",
425
- scopes: ["campaigns:write"],
426
- idempotencyRequired: true,
427
- confirmRequired: true,
428
- description: "Enroll a lead in the latest published campaign version. Triggers the live campaign's automation for that lead.",
429
- }),
430
- op({
431
- id: "campaigns.analytics",
432
- method: "GET",
433
- path: "/api/campaigns/{id}/analytics",
434
- scopes: ["analytics:read"],
435
- description: "Read campaign attribution and performance analytics.",
436
- }),
437
- op({
438
- id: "campaigns.providerCapabilities",
439
- method: "GET",
440
- path: "/api/campaigns/{id}/provider-capabilities",
441
- scopes: ["campaigns:read"],
442
- description: "Evaluate provider capability blockers for a campaign draft.",
443
- }),
444
- op({
445
- id: "campaigns.commentEvents.list",
446
- method: "GET",
447
- path: "/api/campaigns/comment-events",
448
- scopes: ["campaigns:read"],
449
- description: "List matched, suppressed, blocked, and replayed comment automation events.",
450
- // Each event carries verbatim external-commenter text (`body`,
451
- // `commenterDisplayName`), so this read must be framed untrusted even
452
- // though the campaigns group is not blanket-untrusted (AS5).
453
- untrustedContent: true,
454
- }),
455
- op({
456
- id: "campaigns.simulations.list",
457
- method: "GET",
458
- path: "/api/campaigns/{id}/simulations",
459
- scopes: ["campaigns:read"],
460
- description: "List persisted campaign simulation runs.",
461
- }),
462
- op({
463
- id: "campaigns.simulations.create",
464
- method: "POST",
465
- path: "/api/campaigns/{id}/simulations",
466
- scopes: ["campaigns:write"],
467
- idempotencyRequired: true,
468
- description: "Run a persisted dry-run simulation without sending messages.",
469
- }),
470
- op({
471
- id: "campaigns.simulations.get",
472
- method: "GET",
473
- path: "/api/campaigns/{id}/simulations/{simulationId}",
474
- scopes: ["campaigns:read"],
475
- description: "Get a campaign simulation run with trace.",
476
- }),
477
- op({
478
- id: "broadcasts.list",
479
- method: "GET",
480
- path: "/api/broadcasts",
481
- scopes: ["broadcasts:read"],
482
- description: "List broadcasts. Query supports limit, cursor, and status.",
483
- }),
484
- op({
485
- id: "broadcasts.get",
486
- method: "GET",
487
- path: "/api/broadcasts/{id}",
488
- scopes: ["broadcasts:read"],
489
- description: "Get broadcast detail.",
490
- }),
491
- op({
492
- id: "broadcasts.create",
493
- method: "POST",
494
- path: "/api/broadcasts",
495
- scopes: ["broadcasts:write"],
496
- idempotencyRequired: true,
497
- description: "Create a broadcast draft. Body requires campaignId and name.",
498
- }),
499
- op({
500
- id: "broadcasts.preview",
501
- method: "POST",
502
- path: "/api/broadcasts/{id}/preview",
503
- scopes: ["broadcasts:write"],
504
- idempotencyRequired: true,
505
- description: "Refresh broadcast recipient and sample-message preview.",
506
- }),
507
- op({
508
- id: "broadcasts.approve",
509
- method: "POST",
510
- path: "/api/broadcasts/{id}/approve",
511
- scopes: ["broadcasts:write"],
512
- idempotencyRequired: true,
513
- description: "Approve a safe broadcast draft and snapshot recipients.",
514
- }),
515
- op({
516
- id: "broadcasts.launch",
517
- method: "POST",
518
- path: "/api/broadcasts/{id}/launch",
519
- scopes: ["broadcasts:write"],
520
- idempotencyRequired: true,
521
- confirmRequired: true,
522
- description: "Launch an approved broadcast. Sends messages to live leads.",
523
- }),
524
- op({
525
- id: "broadcasts.pause",
526
- method: "POST",
527
- path: "/api/broadcasts/{id}/pause",
528
- scopes: ["broadcasts:write"],
529
- idempotencyRequired: true,
530
- description: "Pause a running broadcast with an optional operator reason.",
531
- }),
532
- op({
533
- id: "broadcasts.retry",
534
- method: "POST",
535
- path: "/api/broadcasts/{id}/retry",
536
- scopes: ["broadcasts:write"],
537
- idempotencyRequired: true,
538
- confirmRequired: true,
539
- description: "Retry failed broadcast recipients after operator review. Re-sends messages to leads.",
540
- }),
541
- op({
542
- id: "broadcasts.cancel",
543
- method: "POST",
544
- path: "/api/broadcasts/{id}/cancel",
545
- scopes: ["broadcasts:write"],
546
- idempotencyRequired: true,
547
- description: "Cancel a draft, approved, running, paused, or failed broadcast.",
548
- }),
549
- op({
550
- id: "webhooks.subscriptions.list",
551
- method: "GET",
552
- path: "/api/webhook-subscriptions",
553
- scopes: ["webhooks:read"],
554
- description: "List outbound webhook subscriptions.",
555
- }),
556
- op({
557
- id: "webhooks.subscriptions.create",
558
- method: "POST",
559
- path: "/api/webhook-subscriptions",
560
- scopes: ["webhooks:write"],
561
- idempotencyRequired: true,
562
- description: "Create an outbound webhook subscription.",
563
- }),
564
- op({
565
- id: "webhooks.subscriptions.update",
566
- method: "PATCH",
567
- path: "/api/webhook-subscriptions/{id}",
568
- scopes: ["webhooks:write"],
569
- idempotencyRequired: true,
570
- description: "Update an outbound webhook subscription.",
571
- }),
572
- op({
573
- id: "webhooks.testDelivery",
574
- method: "POST",
575
- path: "/api/webhook-subscriptions/test-delivery",
576
- scopes: ["webhooks:write"],
577
- idempotencyRequired: true,
578
- description: "Queue a canonical test webhook event.",
579
- }),
580
- op({
581
- id: "webhooks.deliveries.list",
582
- method: "GET",
583
- path: "/api/webhook-subscriptions/deliveries",
584
- scopes: ["webhooks:read"],
585
- description: "List webhook deliveries.",
586
- }),
587
- op({
588
- id: "webhooks.signatureGuide",
589
- method: "GET",
590
- path: "/api/webhook-subscriptions/signature-guide",
591
- scopes: ["webhooks:read"],
592
- description: "Describe Luca webhook HMAC verification.",
593
- }),
594
- op({
595
- id: "webhooks.deliveries.replay",
596
- method: "POST",
597
- path: "/api/webhook-subscriptions/deliveries/{deliveryId}/replay",
598
- scopes: ["webhooks:write"],
599
- idempotencyRequired: true,
600
- confirmRequired: true,
601
- description: "Replay one webhook delivery without changing the event id. Re-triggers the subscriber's downstream automation.",
602
- }),
603
- op({
604
- id: "webhooks.events.replay",
605
- method: "POST",
606
- path: "/api/webhook-subscriptions/events/{eventId}/replay",
607
- scopes: ["webhooks:write"],
608
- idempotencyRequired: true,
609
- confirmRequired: true,
610
- description: "Replay one canonical webhook event. Re-triggers subscribers' downstream automations.",
611
- }),
612
- op({
613
- id: "webhooks.events.bulkReplay",
614
- method: "POST",
615
- path: "/api/webhook-subscriptions/events/replay",
616
- scopes: ["webhooks:write"],
617
- idempotencyRequired: true,
618
- confirmRequired: true,
619
- description: "Bulk replay webhook events by type and time range. Re-triggers downstream automations at scale.",
620
- }),
621
- op({
622
- id: "integrations.connectors.list",
623
- method: "GET",
624
- path: "/api/integrations/connectors",
625
- scopes: ["integrations:read"],
626
- description: "List Zapier, Make, and CRM provider capabilities.",
627
- }),
628
- op({
629
- id: "integrations.crm.oauthUrl",
630
- method: "GET",
631
- path: "/api/integrations/crm/providers/{provider}/oauth-url",
632
- scopes: ["integrations:write"],
633
- description: "Create a CRM provider OAuth URL.",
634
- }),
635
- op({
636
- id: "integrations.crm.oauthCallback",
637
- method: "POST",
638
- path: "/api/integrations/crm/oauth/callback",
639
- scopes: ["integrations:write"],
640
- idempotencyRequired: true,
641
- description: "Exchange a CRM OAuth code and store encrypted credentials.",
642
- }),
643
- op({
644
- id: "integrations.crm.connections.list",
645
- method: "GET",
646
- path: "/api/integrations/crm/connections",
647
- scopes: ["integrations:read"],
648
- description: "List CRM connections.",
649
- }),
650
- op({
651
- id: "integrations.crm.connections.create",
652
- method: "POST",
653
- path: "/api/integrations/crm/connections",
654
- scopes: ["integrations:write"],
655
- idempotencyRequired: true,
656
- description: "Create or update a workspace-scoped CRM connection.",
657
- }),
658
- op({
659
- id: "integrations.crm.connections.schema",
660
- method: "GET",
661
- path: "/api/integrations/crm/connections/{id}/schema",
662
- scopes: ["integrations:read"],
663
- description: "Discover CRM provider schema.",
664
- }),
665
- op({
666
- id: "integrations.crm.connections.health",
667
- method: "POST",
668
- path: "/api/integrations/crm/connections/{id}/health",
669
- scopes: ["integrations:write"],
670
- idempotencyRequired: true,
671
- description: "Probe CRM provider health.",
672
- }),
673
- op({
674
- id: "integrations.crm.connections.webhooksSetup",
675
- method: "POST",
676
- path: "/api/integrations/crm/connections/{id}/webhooks/setup",
677
- scopes: ["integrations:write"],
678
- idempotencyRequired: true,
679
- description: "Configure CRM provider webhook status.",
680
- }),
681
- op({
682
- id: "integrations.crm.mappings.suggest",
683
- method: "POST",
684
- path: "/api/integrations/crm/connections/{id}/mappings/suggest",
685
- scopes: ["integrations:write"],
686
- idempotencyRequired: true,
687
- description: "Suggest CRM field mappings.",
688
- }),
689
- op({
690
- id: "integrations.crm.mappings.confirm",
691
- method: "PUT",
692
- path: "/api/integrations/crm/connections/{id}/mappings",
693
- scopes: ["integrations:write"],
694
- idempotencyRequired: true,
695
- description: "Confirm CRM field mappings.",
696
- }),
697
- op({
698
- id: "integrations.crm.syncRuns.list",
699
- method: "GET",
700
- path: "/api/integrations/crm/connections/{id}/sync-runs",
701
- scopes: ["integrations:read"],
702
- description: "List CRM sync history.",
703
- }),
704
- op({
705
- id: "integrations.crm.syncRuns.create",
706
- method: "POST",
707
- path: "/api/integrations/crm/connections/{id}/sync-runs",
708
- scopes: ["integrations:write"],
709
- idempotencyRequired: true,
710
- description: "Create a CRM dry-run or live sync run.",
711
- }),
712
- op({
713
- id: "integrations.events.list",
714
- method: "GET",
715
- path: "/api/integrations/events",
716
- scopes: ["integrations:read"],
717
- description: "List integration event log.",
718
- }),
719
- op({
720
- id: "voice.profile",
721
- method: "GET",
722
- path: "/api/voice/profile",
723
- scopes: ["voice:read"],
724
- description: "Read a non-verbatim voice-fingerprint summary: version, example count, and last refresh. No corpus text.",
725
- }),
726
- ];
727
- /**
728
- * MCP tool annotations derived from the operation model, so a client can
729
- * reason about a tool before calling it (auto-approve reads, flag destructive
730
- * writes). Hints only — never a security boundary; the Luca API still enforces
731
- * scopes and safety server-side.
732
- */
733
- export function toolAnnotations(operation) {
734
- const readOnly = operation.method === "GET";
735
- return {
736
- title: operation.title,
737
- readOnlyHint: readOnly,
738
- // Destructive = a real-world side effect or an irreversible delete. Only
739
- // meaningful for writes; ordinary create/update is a write but not
740
- // destructive, so a client can treat it with less friction than a launch.
741
- destructiveHint: !readOnly && (operation.confirmRequired || operation.method === "DELETE"),
742
- // Reads are naturally idempotent; flagged writes carry an idempotency-key
743
- // contract that makes a repeat with the same key a no-op.
744
- idempotentHint: readOnly || operation.idempotencyRequired,
745
- // Every tool reaches the external Luca API, outside this server's control.
746
- openWorldHint: true,
747
- };
748
- }
749
- export function operationManifest() {
750
- return LUCA_OPERATIONS.map((operation) => ({
751
- id: operation.id,
752
- group: operation.group,
753
- toolName: operation.toolName,
754
- method: operation.method,
755
- path: operation.path,
756
- scopes: operation.scopes,
757
- idempotencyRequired: operation.idempotencyRequired,
758
- confirmRequired: operation.confirmRequired,
759
- paginates: operation.paginates,
760
- untrustedContent: operation.untrustedContent,
761
- requiredScope: operation.requiredScope,
762
- annotations: toolAnnotations(operation),
763
- description: operation.description,
764
- }));
765
- }
766
- export function operationGroups() {
767
- return Object.entries(LUCA_OPERATION_GROUPS).map(([id, metadata]) => {
768
- const operations = LUCA_OPERATIONS.filter((operation) => operation.group === id);
769
- return {
770
- id: id,
771
- title: metadata.title,
772
- description: metadata.description,
773
- toolCount: operations.length,
774
- scopes: [
775
- ...new Set(operations.flatMap((operation) => operation.scopes)),
776
- ].sort(),
777
- tools: operations.map((operation) => operation.toolName),
778
- };
779
- });
780
- }
1
+ // Public entry point for the operation model. The implementation is split by
2
+ // job under `./operations/`: `registry.ts` holds the types and the `op()`
3
+ // machinery, `catalog.ts` holds the operation table, and `manifest.ts` holds
4
+ // the views derived from it. What a client is told about a tool lives in
5
+ // `../annotations.ts`, one level up, because both kinds of tool ask for it.
6
+ // This file names each export rather than re-exporting with `export *`,
7
+ // because a star barrel pulls the whole dependency graph into every importer.
8
+ // The lint rule that forbids it is measuring real load cost, not style.
9
+ export { LUCA_OPERATIONS } from "./operations/catalog.js";
10
+ export { operationGroups, operationManifest } from "./operations/manifest.js";
11
+ export { confirmInput, DEFAULT_MAX_PAGES, outputSchemaFor, WORKSPACE_INPUT_FIELDS, } from "./operations/registry.js";