@setluca/mcp 0.1.1 → 0.2.0
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/README.md +110 -42
- package/dist/annotations.js +45 -0
- package/dist/completions.js +90 -0
- package/dist/config.js +55 -29
- package/dist/errors.js +59 -14
- 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 +95 -103
- package/dist/index.js +16 -10
- package/dist/oauth-resolver.js +58 -36
- package/dist/observability.js +11 -3
- package/dist/openapi-zod.js +129 -65
- 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 +47 -0
- package/dist/pagination.js +164 -0
- package/dist/prompts.js +63 -8
- package/dist/provenance.js +18 -2
- package/dist/remote.js +80 -49
- package/dist/resilience.js +117 -0
- package/dist/serialization.js +36 -0
- package/dist/server.js +256 -281
- package/dist/task-tools/analytics.js +168 -0
- package/dist/task-tools/bookings.js +154 -0
- package/dist/task-tools/cadences.js +65 -0
- package/dist/task-tools/calls.js +202 -0
- package/dist/task-tools/inbox.js +74 -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 +36 -33
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const leadsOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "leads.list",
|
|
5
|
+
title: "List leads",
|
|
6
|
+
description: "List leads. Query supports limit, cursor, channel, and q according to Luca OpenAPI.",
|
|
7
|
+
redactedFields: ["leads[].displayName", "leads[].externalUserId"],
|
|
8
|
+
}),
|
|
9
|
+
op({
|
|
10
|
+
id: "leads.get",
|
|
11
|
+
title: "Get a lead's timeline",
|
|
12
|
+
description: "Get a lead's full timeline and notes.",
|
|
13
|
+
redactedFields: [
|
|
14
|
+
"identities[].displayName",
|
|
15
|
+
"identities[].externalUserId",
|
|
16
|
+
"lead.customFields",
|
|
17
|
+
"lead.displayName",
|
|
18
|
+
"messages[].body",
|
|
19
|
+
"messages[].media",
|
|
20
|
+
"notes[].body",
|
|
21
|
+
],
|
|
22
|
+
}),
|
|
23
|
+
op({
|
|
24
|
+
id: "leads.explain",
|
|
25
|
+
title: "Explain why a lead qualified",
|
|
26
|
+
description: "Explain why a lead qualified: the qualifier's reasoning, signals, and confidence from its most recent drafted turn.",
|
|
27
|
+
redactedFields: ["qualification"],
|
|
28
|
+
}),
|
|
29
|
+
op({
|
|
30
|
+
id: "leads.create",
|
|
31
|
+
title: "Create or upsert a lead",
|
|
32
|
+
description: "Create or upsert a lead identity. Body requires channel and externalUserId.",
|
|
33
|
+
redactedFields: [
|
|
34
|
+
"lead.customFields",
|
|
35
|
+
"lead.displayName",
|
|
36
|
+
"lead.externalUserId",
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
op({
|
|
40
|
+
id: "leads.update",
|
|
41
|
+
title: "Update a lead",
|
|
42
|
+
description: "Update a lead's display name, stage, score, tags, or custom fields.",
|
|
43
|
+
redactedFields: [
|
|
44
|
+
"lead.customFields",
|
|
45
|
+
"lead.displayName",
|
|
46
|
+
"lead.externalUserId",
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
op({
|
|
50
|
+
id: "leads.fieldDefinitions.list",
|
|
51
|
+
title: "List custom lead fields",
|
|
52
|
+
description: "List custom lead field definitions.",
|
|
53
|
+
}),
|
|
54
|
+
op({
|
|
55
|
+
id: "leads.fieldDefinitions.upsert",
|
|
56
|
+
title: "Define a custom lead field",
|
|
57
|
+
description: "Create or update a custom lead field definition.",
|
|
58
|
+
}),
|
|
59
|
+
op({
|
|
60
|
+
id: "leads.importPreview",
|
|
61
|
+
title: "Preview a lead import",
|
|
62
|
+
description: "Preview a mapped CSV lead import before applying it.",
|
|
63
|
+
}),
|
|
64
|
+
op({
|
|
65
|
+
id: "leads.importApply",
|
|
66
|
+
title: "Apply a lead import",
|
|
67
|
+
description: "Apply a mapped lead import with consent evidence.",
|
|
68
|
+
}),
|
|
69
|
+
op({
|
|
70
|
+
id: "leads.identities.attach",
|
|
71
|
+
title: "Attach a channel identity to a lead",
|
|
72
|
+
description: "Attach a verified or claimed identity to a lead.",
|
|
73
|
+
}),
|
|
74
|
+
op({
|
|
75
|
+
id: "leads.consents.grant",
|
|
76
|
+
title: "Record a lead's consent",
|
|
77
|
+
description: "Grant explicit lead consent for a channel purpose.",
|
|
78
|
+
}),
|
|
79
|
+
op({
|
|
80
|
+
id: "leads.consents.revoke",
|
|
81
|
+
title: "Revoke a lead's consent",
|
|
82
|
+
description: "Revoke explicit lead consent for a channel purpose.",
|
|
83
|
+
}),
|
|
84
|
+
op({
|
|
85
|
+
id: "leads.notes.add",
|
|
86
|
+
title: "Add a note to a lead",
|
|
87
|
+
description: "Append a coach note to a lead. Body requires body.",
|
|
88
|
+
redactedFields: ["note.body"],
|
|
89
|
+
}),
|
|
90
|
+
op({
|
|
91
|
+
id: "leads.draft",
|
|
92
|
+
title: "Draft a reply to a lead",
|
|
93
|
+
description: "Draft a voice-matched reply for a lead on demand and queue it for review. Optional guidance steers tone or content.",
|
|
94
|
+
redactedFields: ["draft.body"],
|
|
95
|
+
}),
|
|
96
|
+
op({
|
|
97
|
+
id: "leads.notes.delete",
|
|
98
|
+
title: "Delete a lead note",
|
|
99
|
+
description: "Remove one coach note from a lead and leave the rest of the timeline alone. This is how a note filed by mistake gets taken back.",
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const learningOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "learning.panel",
|
|
5
|
+
title: "Read what Luca wants to learn",
|
|
6
|
+
description: "Read which of the three profile questions Luca still owes an answer to, with suggested answers inferred from the coach's own words. The worked example pair is blanked for a redacted-tier key.",
|
|
7
|
+
redactedFields: ["exampleDm", "exampleDraft"],
|
|
8
|
+
}),
|
|
9
|
+
op({
|
|
10
|
+
id: "learning.frameworkPerformance",
|
|
11
|
+
title: "Read framework objection performance",
|
|
12
|
+
description: "Win rate by objection bucket for the sales framework the coach has configured right now, over a rolling window. These numbers are correlational \u2014 read them as a signal, not as proof a framework caused the result.",
|
|
13
|
+
}),
|
|
14
|
+
op({
|
|
15
|
+
id: "learning.proposals.list",
|
|
16
|
+
title: "List learning proposals",
|
|
17
|
+
description: "List what the coach's tests have learned and are proposing they act on, newest first. Dismissed proposals are left out unless you ask for them.",
|
|
18
|
+
}),
|
|
19
|
+
op({
|
|
20
|
+
id: "learning.proposals.dismiss",
|
|
21
|
+
title: "Dismiss a learning proposal",
|
|
22
|
+
description: "Dismiss one learning proposal so it drops out of the default list. Use it when the coach has decided against the change, not when you have simply read it.",
|
|
23
|
+
}),
|
|
24
|
+
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const memoryOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "memory.list",
|
|
5
|
+
title: "List what Luca remembers",
|
|
6
|
+
description: "List what Luca remembers, filtered by lead, scope, source, trust level, or free text. The fact, the reasoning, and the quoted evidence are blanked for a redacted-tier key; trust level, lead, and dates always come back.",
|
|
7
|
+
redactedFields: [
|
|
8
|
+
"memories[].content",
|
|
9
|
+
"memories[].evidence",
|
|
10
|
+
"memories[].reason",
|
|
11
|
+
],
|
|
12
|
+
}),
|
|
13
|
+
op({
|
|
14
|
+
id: "memory.update",
|
|
15
|
+
title: "Edit a memory",
|
|
16
|
+
// The corrected memory comes back in full, and a memory is a fact Luca
|
|
17
|
+
// extracted from what a lead wrote.
|
|
18
|
+
untrustedContent: true,
|
|
19
|
+
description: "Correct one memory: edit the fact, flag it disputed when it turns out to be wrong, restore an archived one, or make it win over a conflicting memory.",
|
|
20
|
+
redactedFields: ["content", "evidence", "reason"],
|
|
21
|
+
}),
|
|
22
|
+
op({
|
|
23
|
+
id: "memory.setArchived",
|
|
24
|
+
title: "Archive or restore memories",
|
|
25
|
+
description: "Archive or restore several memories at once. Archiving stops Luca using a fact while the record and its provenance survive, so it is always reversible.",
|
|
26
|
+
}),
|
|
27
|
+
];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const reportsOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "reports.scorecard",
|
|
5
|
+
title: "Read the scorecard",
|
|
6
|
+
description: "Read the sales scorecard for a window: calls booked and completed, show rate, won and lost, draft approvals, and hours saved.",
|
|
7
|
+
}),
|
|
8
|
+
op({
|
|
9
|
+
id: "reports.morning",
|
|
10
|
+
title: "Read the morning report",
|
|
11
|
+
description: "Structured 24h review-queue digest (items + counts) for the coach. No model call; summarize client-side.",
|
|
12
|
+
redactedFields: ["items[].draftBody"],
|
|
13
|
+
}),
|
|
14
|
+
];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const reviewQueueOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "reviewQueue.list",
|
|
5
|
+
title: "List the review queue",
|
|
6
|
+
description: "List review-queue items when the API key has review queue access.",
|
|
7
|
+
redactedFields: [
|
|
8
|
+
"items[].criticIssues",
|
|
9
|
+
"items[].draftBody",
|
|
10
|
+
"items[].draftParts",
|
|
11
|
+
"items[].leadDisplayName",
|
|
12
|
+
"items[].media",
|
|
13
|
+
"items[].mediaContext",
|
|
14
|
+
"items[].objectionVariants",
|
|
15
|
+
"items[].qualification",
|
|
16
|
+
"items[].safetyFindings",
|
|
17
|
+
"items[].signals",
|
|
18
|
+
],
|
|
19
|
+
}),
|
|
20
|
+
op({
|
|
21
|
+
id: "reviewQueue.explain",
|
|
22
|
+
title: "Explain a queued draft",
|
|
23
|
+
description: "Explain the signals behind a drafted reply: its voice-match score, any critic findings, and the qualification that motivated it.",
|
|
24
|
+
redactedFields: ["criticIssues", "draftBody", "qualification"],
|
|
25
|
+
}),
|
|
26
|
+
op({
|
|
27
|
+
id: "reviewQueue.approve",
|
|
28
|
+
title: "Approve and send a queued draft",
|
|
29
|
+
confirmRequired: true,
|
|
30
|
+
description: "Approve a pending draft for sending; dispatches through the lead's channel. Sends a real message.",
|
|
31
|
+
}),
|
|
32
|
+
op({
|
|
33
|
+
id: "reviewQueue.reject",
|
|
34
|
+
title: "Reject a queued draft",
|
|
35
|
+
description: "Reject a pending review-queue draft so it never sends. Stops any related booking-recovery cadence and emits review.rejected.",
|
|
36
|
+
}),
|
|
37
|
+
op({
|
|
38
|
+
id: "reviewQueue.restore",
|
|
39
|
+
title: "Restore a rejected draft",
|
|
40
|
+
description: "Restore a rejected review-queue item back to pending. Fails if it's a booking message whose recovery window already passed.",
|
|
41
|
+
}),
|
|
42
|
+
op({
|
|
43
|
+
id: "reviewQueue.media.retry",
|
|
44
|
+
title: "Retry a draft's failed media",
|
|
45
|
+
description: "Retry a failed voice-note transcription so the draft can be reviewed. Only re-transcribes; never redrafts or sends.",
|
|
46
|
+
}),
|
|
47
|
+
op({
|
|
48
|
+
id: "reviewQueue.sla",
|
|
49
|
+
title: "Check the review queue's response times",
|
|
50
|
+
description: "Read median first-response time and its daily trend. Tells you whether the queue is being cleared fast enough.",
|
|
51
|
+
}),
|
|
52
|
+
op({
|
|
53
|
+
id: "reviewQueue.objectionVariant",
|
|
54
|
+
title: "Record an objection variant choice",
|
|
55
|
+
description: "Record which objection-specialist variant was picked for one queued draft. It is how the objection tests learn which handling works, so record the variant the coach actually chose.",
|
|
56
|
+
}),
|
|
57
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const safetyOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "safety.accounts",
|
|
5
|
+
title: "Check connected account safety",
|
|
6
|
+
description: "Read each connected channel account's safety status, 24-hour send count, and human-approval window. An account inside its approval window must not be automated against.",
|
|
7
|
+
}),
|
|
8
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const usageOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "usage.spend",
|
|
5
|
+
title: "Read this period's spend",
|
|
6
|
+
description: "Read model spend for the period, rolled up by agent and by day, against the plan's cap.",
|
|
7
|
+
}),
|
|
8
|
+
op({
|
|
9
|
+
id: "usage.messages",
|
|
10
|
+
title: "Read this period's message count",
|
|
11
|
+
description: "Read the message pool: used, allowed, and remaining for the period. Check this before starting work that drafts in bulk.",
|
|
12
|
+
}),
|
|
13
|
+
];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const voiceOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "voice.profile",
|
|
5
|
+
title: "Read the coach's voice fingerprint",
|
|
6
|
+
description: "Read a non-verbatim voice-fingerprint summary: version, example count, and last refresh. No corpus text.",
|
|
7
|
+
}),
|
|
8
|
+
op({
|
|
9
|
+
id: "voice.corpus",
|
|
10
|
+
title: "Read the coach's voice corpus",
|
|
11
|
+
description: "List the coach's voice corpus: the writing every draft is matched against. Entry id, source, channel, and age always come back; the writing itself is blanked for a redacted-tier key.",
|
|
12
|
+
redactedFields: ["entries[].content"],
|
|
13
|
+
}),
|
|
14
|
+
];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { op } from "../registry.js";
|
|
2
|
+
export const webhooksOperations = [
|
|
3
|
+
op({
|
|
4
|
+
id: "webhooks.subscriptions.list",
|
|
5
|
+
title: "List webhook subscriptions",
|
|
6
|
+
description: "List outbound webhook subscriptions.",
|
|
7
|
+
}),
|
|
8
|
+
op({
|
|
9
|
+
id: "webhooks.subscriptions.create",
|
|
10
|
+
title: "Create a webhook subscription",
|
|
11
|
+
// Naturally idempotent: an active subscription with the same target URL
|
|
12
|
+
// is updated in place (target URLs are unique per Zap/scenario webhook),
|
|
13
|
+
// so this route requires no Idempotency-Key (see the matching comment on
|
|
14
|
+
// its policy entry in apps/api/src/lib/public-route-policy.ts).
|
|
15
|
+
description: "Create an outbound webhook subscription.",
|
|
16
|
+
}),
|
|
17
|
+
op({
|
|
18
|
+
id: "webhooks.subscriptions.update",
|
|
19
|
+
title: "Update a webhook subscription",
|
|
20
|
+
description: "Update an outbound webhook subscription.",
|
|
21
|
+
}),
|
|
22
|
+
op({
|
|
23
|
+
id: "webhooks.subscriptions.delete",
|
|
24
|
+
title: "Delete a webhook subscription",
|
|
25
|
+
// Deleting by id is naturally idempotent (the route matches its policy
|
|
26
|
+
// entry, which carries no requireIdempotency), so no Idempotency-Key is
|
|
27
|
+
// sent and it stays out of the confirm-gated tier that re-triggers live
|
|
28
|
+
// downstream automation; re-running the delete is a safe no-op.
|
|
29
|
+
description: "Delete an outbound webhook subscription. Terminal, idempotent soft delete: delivery history stays queryable and the subscription can never be resurrected.",
|
|
30
|
+
}),
|
|
31
|
+
op({
|
|
32
|
+
id: "webhooks.testDelivery",
|
|
33
|
+
title: "Send a test webhook",
|
|
34
|
+
description: "Queue a canonical test webhook event.",
|
|
35
|
+
}),
|
|
36
|
+
op({
|
|
37
|
+
id: "webhooks.deliveries.list",
|
|
38
|
+
title: "List webhook deliveries",
|
|
39
|
+
description: "List webhook deliveries.",
|
|
40
|
+
}),
|
|
41
|
+
op({
|
|
42
|
+
id: "webhooks.signatureGuide",
|
|
43
|
+
title: "Read the webhook signature guide",
|
|
44
|
+
description: "Describe Luca webhook HMAC verification.",
|
|
45
|
+
}),
|
|
46
|
+
op({
|
|
47
|
+
id: "webhooks.deliveries.replay",
|
|
48
|
+
title: "Replay a webhook delivery",
|
|
49
|
+
confirmRequired: true,
|
|
50
|
+
description: "Replay one webhook delivery without changing the event id. Re-triggers the subscriber's downstream automation.",
|
|
51
|
+
}),
|
|
52
|
+
op({
|
|
53
|
+
id: "webhooks.events.list",
|
|
54
|
+
title: "List webhook events",
|
|
55
|
+
description: "List canonical webhook events of one type, newest first, in the delivered payload fields. Falls back to a documented sample payload when the workspace has none yet.",
|
|
56
|
+
// Not framed untrusted: every event's delivered payload is structured ids,
|
|
57
|
+
// enums, and counts (e.g. `message.received` is
|
|
58
|
+
// `{ leadId, conversationId, channel, providerMessageId }`) — no verbatim
|
|
59
|
+
// lead text, draft body, or PII. The one content-shaped field in the whole
|
|
60
|
+
// catalog is a fictional `text` in the "no events yet" sample fallback,
|
|
61
|
+
// which the live pipeline never emits. Contrast `campaigns.commentEvents.list`,
|
|
62
|
+
// which reads real verbatim commenter text and stays untrusted.
|
|
63
|
+
}),
|
|
64
|
+
op({
|
|
65
|
+
id: "webhooks.events.replay",
|
|
66
|
+
title: "Replay a webhook event",
|
|
67
|
+
confirmRequired: true,
|
|
68
|
+
description: "Replay one canonical webhook event. Re-triggers subscribers' downstream automations.",
|
|
69
|
+
}),
|
|
70
|
+
op({
|
|
71
|
+
id: "webhooks.events.bulkReplay",
|
|
72
|
+
title: "Replay a batch of webhook events",
|
|
73
|
+
confirmRequired: true,
|
|
74
|
+
description: "Bulk replay webhook events by type and time range. Re-triggers downstream automations at scale.",
|
|
75
|
+
}),
|
|
76
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Views derived from the operation table. Nothing here holds state of its own;
|
|
2
|
+
// each function reads `LUCA_OPERATIONS` and reshapes it for a caller — the
|
|
3
|
+
// `luca://operations` resource, the docs generator, the registry check.
|
|
4
|
+
import { operationAnnotations } from "../annotations.js";
|
|
5
|
+
import { LUCA_OPERATIONS } from "./catalog.js";
|
|
6
|
+
import { LUCA_OPERATION_GROUPS } from "./registry.js";
|
|
7
|
+
export function operationManifest() {
|
|
8
|
+
return LUCA_OPERATIONS.map((operation) => ({
|
|
9
|
+
id: operation.id,
|
|
10
|
+
group: operation.group,
|
|
11
|
+
toolName: operation.toolName,
|
|
12
|
+
method: operation.method,
|
|
13
|
+
path: operation.path,
|
|
14
|
+
scopes: operation.scopes,
|
|
15
|
+
idempotencyRequired: operation.idempotencyRequired,
|
|
16
|
+
confirmRequired: operation.confirmRequired,
|
|
17
|
+
paginates: operation.pageShape !== undefined,
|
|
18
|
+
untrustedContent: operation.untrustedContent,
|
|
19
|
+
requiredScope: operation.requiredScope,
|
|
20
|
+
annotations: operationAnnotations(operation),
|
|
21
|
+
description: operation.description,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export function operationGroups() {
|
|
25
|
+
return Object.entries(LUCA_OPERATION_GROUPS).map(([id, metadata]) => {
|
|
26
|
+
const operations = LUCA_OPERATIONS.filter((operation) => operation.group === id);
|
|
27
|
+
return {
|
|
28
|
+
// SAFETY: Object.entries widens the key to `string`; the cast recovers
|
|
29
|
+
// what TypeScript already knows about LUCA_OPERATION_GROUPS's own keys.
|
|
30
|
+
id: id,
|
|
31
|
+
title: metadata.title,
|
|
32
|
+
description: metadata.description,
|
|
33
|
+
toolCount: operations.length,
|
|
34
|
+
scopes: [
|
|
35
|
+
...new Set(operations.flatMap((operation) => operation.scopes)),
|
|
36
|
+
].sort(),
|
|
37
|
+
tools: operations.map((operation) => operation.toolName),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|