@setluca/mcp 0.3.1 → 0.3.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/CHANGELOG.md +28 -0
- package/dist/annotations.js +1 -1
- package/dist/operations/groups/analytics.js +2 -2
- package/dist/operations/groups/bookings.js +17 -1
- package/dist/operations/groups/callEvents.js +2 -0
- package/dist/operations/groups/insights.js +1 -0
- package/dist/operations/groups/integrations.js +5 -1
- package/dist/operations/groups/knowledge.js +1 -0
- package/dist/operations/registry.js +1 -0
- package/dist/resilience.js +9 -11
- package/dist/task-tools/analytics.js +2 -2
- package/dist/task-tools/calls.js +6 -5
- package/dist/tool.js +6 -8
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,34 @@ input schema, or changing what a tool does to a workspace.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.3.3] - 2026-10-06
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Mark live booking provider and calendar reads, plus call feedback that can
|
|
18
|
+
start no-show recovery, as reaching outside the Luca workspace in MCP tool
|
|
19
|
+
annotations.
|
|
20
|
+
- Mark booking outcomes that can queue CRM deal-stage updates as reaching
|
|
21
|
+
outside Luca, calendar selection as reading Google, and CRM OAuth URL creation
|
|
22
|
+
as a state-changing GET. State-changing GETs no longer retry without an
|
|
23
|
+
idempotency key.
|
|
24
|
+
- Require confirmation for won or lost booking outcomes, directly and through
|
|
25
|
+
the call-closing task tool, before either can queue a connected CRM deal update.
|
|
26
|
+
Existing calls without `confirm: true` now receive `confirmation_required`;
|
|
27
|
+
this safety change is intentionally included in 0.3.3 despite the normal
|
|
28
|
+
major-version rule for a changed tool-call contract.
|
|
29
|
+
- Keep unexpected tool and transport error messages out of client results and
|
|
30
|
+
Worker logs.
|
|
31
|
+
|
|
32
|
+
## [0.3.2] - 2026-10-06
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Correct MCP safety hints for CRM schema and health probes, knowledge-source
|
|
37
|
+
syncs, and the settings read that creates defaults on first use.
|
|
38
|
+
- Clarify the display names and descriptions of the ghosted-lead analytics and
|
|
39
|
+
weekly business report tools without changing their stable tool names.
|
|
40
|
+
|
|
13
41
|
## [0.3.1] - 2026-10-06
|
|
14
42
|
|
|
15
43
|
### Fixed
|
package/dist/annotations.js
CHANGED
|
@@ -8,7 +8,7 @@ import { optionalField } from "./optional-field.js";
|
|
|
8
8
|
export function operationAnnotationSource(operation) {
|
|
9
9
|
return {
|
|
10
10
|
title: operation.title,
|
|
11
|
-
readOnly: operation.
|
|
11
|
+
readOnly: operation.readOnly,
|
|
12
12
|
mutatesExisting: operation.mutatesExisting,
|
|
13
13
|
...optionalField("confirm", operation.confirm),
|
|
14
14
|
openWorld: operation.openWorld,
|
|
@@ -36,8 +36,8 @@ export const analyticsOperations = [
|
|
|
36
36
|
}),
|
|
37
37
|
op({
|
|
38
38
|
id: "analytics.ghostedLeads",
|
|
39
|
-
title: "
|
|
40
|
-
description: "
|
|
39
|
+
title: "Analyze leads who stopped replying",
|
|
40
|
+
description: "Summarize leads who stopped replying during a period, including their count, follow-up recovery rate, estimated lost revenue, and common reasons conversations stalled. This is an aggregate report, not a list of individual leads.",
|
|
41
41
|
// Each top-killer cluster label is free text drawn from lead messages.
|
|
42
42
|
untrustedContent: true,
|
|
43
43
|
}),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isJsonObject } from "../../serialization.js";
|
|
1
2
|
import { op } from "../registry.js";
|
|
2
3
|
export const bookingsOperations = [
|
|
3
4
|
op({
|
|
@@ -29,11 +30,15 @@ export const bookingsOperations = [
|
|
|
29
30
|
op({
|
|
30
31
|
id: "bookings.availability",
|
|
31
32
|
title: "Read open call slots",
|
|
33
|
+
// Availability comes from the connected booking provider.
|
|
34
|
+
openWorld: true,
|
|
32
35
|
description: "List open slots the coach can actually be booked into. Check this before booking.",
|
|
33
36
|
}),
|
|
34
37
|
op({
|
|
35
38
|
id: "bookings.managedAvailability",
|
|
36
39
|
title: "Read Luca-managed call slots",
|
|
40
|
+
// Google Calendar removes conflicts from Luca-managed slots.
|
|
41
|
+
openWorld: true,
|
|
37
42
|
description: "List open slots for one of Luca's own call types, with calendar conflicts already removed.",
|
|
38
43
|
}),
|
|
39
44
|
op({
|
|
@@ -59,6 +64,8 @@ export const bookingsOperations = [
|
|
|
59
64
|
op({
|
|
60
65
|
id: "bookings.providers.get",
|
|
61
66
|
title: "Get a booking provider",
|
|
67
|
+
// Event types and calendars are fetched from the provider account.
|
|
68
|
+
openWorld: true,
|
|
62
69
|
description: "List the event types and calendars in one connected provider account.",
|
|
63
70
|
}),
|
|
64
71
|
op({
|
|
@@ -127,7 +134,14 @@ export const bookingsOperations = [
|
|
|
127
134
|
id: "bookings.outcome.record",
|
|
128
135
|
title: "Record a booking's outcome",
|
|
129
136
|
mutatesExisting: true,
|
|
130
|
-
|
|
137
|
+
// Won and lost outcomes queue a deal-stage change in the connected CRM.
|
|
138
|
+
openWorld: true,
|
|
139
|
+
confirm: {
|
|
140
|
+
applies: (input) => isJsonObject(input.body) &&
|
|
141
|
+
(input.body.status === "won" || input.body.status === "lost"),
|
|
142
|
+
description: "status is won or lost (can update a connected CRM deal)",
|
|
143
|
+
},
|
|
144
|
+
description: "Record what the call was worth: won, lost with a reason, or still open. Won and lost outcomes can update a connected CRM deal stage, so pass confirm: true for those outcomes. To correct an earlier outcome send the expectedVersion you read; a stale version returns 409 with the current one.",
|
|
131
145
|
}),
|
|
132
146
|
op({
|
|
133
147
|
id: "bookings.lifecycleResolution",
|
|
@@ -206,6 +220,8 @@ export const bookingsOperations = [
|
|
|
206
220
|
id: "bookings.calendars.select",
|
|
207
221
|
title: "Select a booking calendar",
|
|
208
222
|
mutatesExisting: true,
|
|
223
|
+
// Selection checks that the calendar exists in the connected Google account.
|
|
224
|
+
openWorld: true,
|
|
209
225
|
description: "Add or update a calendar selection, either for conflict checks or as the destination new bookings are written to. The destination has to be writable: a read-only calendar returns 400, and a Google connection needing attention returns 409.",
|
|
210
226
|
}),
|
|
211
227
|
op({
|
|
@@ -27,6 +27,8 @@ export const callEventsOperations = [
|
|
|
27
27
|
op({
|
|
28
28
|
id: "callEvents.feedback",
|
|
29
29
|
title: "Report how a call went",
|
|
30
|
+
// A no-show report can start recovery messaging to the lead.
|
|
31
|
+
openWorld: true,
|
|
30
32
|
mutatesExisting: true,
|
|
31
33
|
untrustedContent: true,
|
|
32
34
|
// Only a no_show can start recovery that messages the lead, so only that
|
|
@@ -26,6 +26,7 @@ export const insightsOperations = [
|
|
|
26
26
|
op({
|
|
27
27
|
id: "insights.settings.get",
|
|
28
28
|
title: "Read alert delivery settings",
|
|
29
|
+
readOnly: false,
|
|
29
30
|
description: "Read which insight alerts reach the coach and how. The row is created with defaults on the first read, so this never 404s. Check it before concluding a quiet feed means nothing is happening \u2014 it may just be muted.",
|
|
30
31
|
}),
|
|
31
32
|
op({
|
|
@@ -8,7 +8,9 @@ export const integrationsOperations = [
|
|
|
8
8
|
op({
|
|
9
9
|
id: "integrations.crm.oauthUrl",
|
|
10
10
|
title: "Start a CRM connection",
|
|
11
|
-
|
|
11
|
+
// This GET stores a fresh OAuth state record for the callback.
|
|
12
|
+
readOnly: false,
|
|
13
|
+
description: "Create a CRM provider OAuth URL and store its one-time state.",
|
|
12
14
|
}),
|
|
13
15
|
op({
|
|
14
16
|
id: "integrations.crm.connections.list",
|
|
@@ -24,6 +26,7 @@ export const integrationsOperations = [
|
|
|
24
26
|
op({
|
|
25
27
|
id: "integrations.crm.connections.schema",
|
|
26
28
|
title: "Read a CRM's field schema",
|
|
29
|
+
openWorld: true,
|
|
27
30
|
// sampleRecords are literal contact and deal rows from the coach's CRM, which leads and other people filled in.
|
|
28
31
|
untrustedContent: true,
|
|
29
32
|
description: "Discover CRM provider schema.",
|
|
@@ -31,6 +34,7 @@ export const integrationsOperations = [
|
|
|
31
34
|
op({
|
|
32
35
|
id: "integrations.crm.connections.health",
|
|
33
36
|
title: "Check a CRM connection's health",
|
|
37
|
+
openWorld: true,
|
|
34
38
|
description: "Probe CRM provider health.",
|
|
35
39
|
}),
|
|
36
40
|
op({
|
|
@@ -27,6 +27,7 @@ export const knowledgeOperations = [
|
|
|
27
27
|
id: "knowledge.sources.sync",
|
|
28
28
|
title: "Re-sync a knowledge source",
|
|
29
29
|
mutatesExisting: true,
|
|
30
|
+
openWorld: true,
|
|
30
31
|
description: "Queue a re-read of one knowledge source now rather than waiting for its next scheduled sync. It returns as soon as the job is queued, so any new suggestions appear on a later read.",
|
|
31
32
|
}),
|
|
32
33
|
op({
|
|
@@ -291,6 +291,7 @@ export function op(input) {
|
|
|
291
291
|
path,
|
|
292
292
|
scopes,
|
|
293
293
|
mutatesExisting: mutatesExistingRoute,
|
|
294
|
+
readOnly: input.readOnly ?? method === "GET",
|
|
294
295
|
idempotencyRequired,
|
|
295
296
|
...optionalField("confirm", input.confirm),
|
|
296
297
|
openWorld: input.openWorld ?? false,
|
package/dist/resilience.js
CHANGED
|
@@ -8,16 +8,14 @@ import * as Schedule from "effect/Schedule";
|
|
|
8
8
|
import * as Str from "effect/String";
|
|
9
9
|
import { formatError, LucaHttpError, LucaTimeoutError, } from "./errors.js";
|
|
10
10
|
/**
|
|
11
|
-
* The statuses worth
|
|
12
|
-
* handler, so a replay
|
|
11
|
+
* The statuses worth considering for a retry. A gateway can answer after the
|
|
12
|
+
* handler ran, so a replay is still subject to read-only or idempotency rules.
|
|
13
13
|
*
|
|
14
14
|
* 500 is deliberately not here. A 500 means the handler ran and threw, which
|
|
15
15
|
* may well be after it wrote something. Replaying that without an idempotency
|
|
16
16
|
* key can double the write.
|
|
17
17
|
*/
|
|
18
18
|
const RETRYABLE_STATUSES = HashSet.fromIterable([429, 502, 503, 504]);
|
|
19
|
-
/** Methods with no effect to repeat, so a replay is safe with or without a key. */
|
|
20
|
-
const REPLAYABLE_METHODS = HashSet.fromIterable(["GET", "HEAD"]);
|
|
21
19
|
/**
|
|
22
20
|
* The longest server-requested wait we will sit through. Past this the server
|
|
23
21
|
* is not asking for a retry, it is asking us to come back later. Holding a
|
|
@@ -74,7 +72,7 @@ function isTransient(error) {
|
|
|
74
72
|
: HashSet.has(TRANSIENT_TAGS, error._tag);
|
|
75
73
|
}
|
|
76
74
|
/**
|
|
77
|
-
* Whether this failure may be sent again.
|
|
75
|
+
* Whether this failure may be sent again. Read-only calls are replayable. A write
|
|
78
76
|
* is replayable only when it carries an idempotency key, because the API dedupes
|
|
79
77
|
* by [organizationId, coachId, actorId, method, path, key] and that is the only
|
|
80
78
|
* replay which cannot file the same write twice.
|
|
@@ -89,8 +87,7 @@ export function mayRetry(policy, error) {
|
|
|
89
87
|
if (Option.exists(requestedDelay(error), (ms) => ms > MAX_RETRY_AFTER_MS)) {
|
|
90
88
|
return false;
|
|
91
89
|
}
|
|
92
|
-
return
|
|
93
|
-
policy.idempotencyKey !== undefined);
|
|
90
|
+
return policy.readOnly || policy.idempotencyKey !== undefined;
|
|
94
91
|
}
|
|
95
92
|
/**
|
|
96
93
|
* The backoff schedule with a `Retry-After` folded in: the next replay waits
|
|
@@ -124,13 +121,13 @@ function lastFailureNote(lastFailure) {
|
|
|
124
121
|
});
|
|
125
122
|
}
|
|
126
123
|
/**
|
|
127
|
-
* The timeout a caller reads. A read can be sent again. A
|
|
128
|
-
* may or may not have landed, so the message says
|
|
129
|
-
* makes a second send safe.
|
|
124
|
+
* The timeout a caller reads. A read-only call can be sent again. A call that
|
|
125
|
+
* changes state and timed out may or may not have landed, so the message says
|
|
126
|
+
* so and names the key that makes a second send safe when there is one.
|
|
130
127
|
*/
|
|
131
128
|
function timeoutError(policy, ms, lastFailure = Option.none()) {
|
|
132
129
|
const base = `Luca API did not answer within ${ms}ms${lastFailureNote(lastFailure)}`;
|
|
133
|
-
if (
|
|
130
|
+
if (policy.readOnly) {
|
|
134
131
|
return new LucaTimeoutError({ message: base });
|
|
135
132
|
}
|
|
136
133
|
const replay = policy.idempotencyKey === undefined
|
|
@@ -163,6 +160,7 @@ export function resilient(request, timeoutMs, attempt, deadlineMs = CALL_DEADLIN
|
|
|
163
160
|
const lastFailure = yield* Ref.make(Option.none());
|
|
164
161
|
const policy = {
|
|
165
162
|
method: request.operation.method,
|
|
163
|
+
readOnly: request.operation.readOnly,
|
|
166
164
|
idempotencyKey,
|
|
167
165
|
};
|
|
168
166
|
return yield* attempt(idempotencyKey).pipe(Effect.timeoutOrElse({
|
|
@@ -177,8 +177,8 @@ const getAnalytics = taskTool({
|
|
|
177
177
|
});
|
|
178
178
|
const magicMonday = taskTool({
|
|
179
179
|
name: "luca_analytics_magic_monday",
|
|
180
|
-
title: "
|
|
181
|
-
description: "Return the
|
|
180
|
+
title: "Read the weekly business report",
|
|
181
|
+
description: "Return the current week's business report (called Magic Monday in Luca): pipeline funnel, revenue and ROI, response-speed impact, a point-in-time revenue forecast, and call intelligence (show/close rates). Composes five analytics reads into a single call. Example: call with {} for the weekly report.",
|
|
182
182
|
inputSchema: {
|
|
183
183
|
...WORKSPACE_INPUT_FIELDS,
|
|
184
184
|
},
|
package/dist/task-tools/calls.js
CHANGED
|
@@ -91,12 +91,13 @@ function outcomeBody(input) {
|
|
|
91
91
|
const closeCallLoop = taskTool({
|
|
92
92
|
name: "luca_close_call_loop",
|
|
93
93
|
title: "Close the loop on a call",
|
|
94
|
-
description: 'File what happened on a call and what it was worth, in one call. Reports attendance on the call event (completed, no_show, rescheduled, or cancelled) and, when an outcome is given, records the booking outcome: won with an amount, lost with a reason id from luca_bookings_outcome_reasons_list, or open with the next follow-up. This tool sends no message to the lead itself, but the records it files start work in the API. Reporting a no_show can start no-show recovery, which schedules a message to the lead when the booking type has recovery on
|
|
95
|
-
//
|
|
96
|
-
// report needs approval. The other attendance values stay ungated.
|
|
94
|
+
description: 'File what happened on a call and what it was worth, in one call. Reports attendance on the call event (completed, no_show, rescheduled, or cancelled) and, when an outcome is given, records the booking outcome: won with an amount, lost with a reason id from luca_bookings_outcome_reasons_list, or open with the next follow-up. This tool sends no message to the lead itself, but the records it files start work in the API. Reporting a no_show can start no-show recovery, which schedules a message to the lead when the booking type has recovery on. A completed call queues its summary, which can later sync to the connected CRM. A won or lost outcome queues a CRM deal-stage update, and lost also moves the lead to lost. Pass confirm: true before reporting a no_show or recording a won or lost outcome. Example: { bookingId, attendance: "completed", outcome: "won", amountMinor: 250000, currency: "USD", confirm: true }.',
|
|
95
|
+
// Recovery messaging and CRM deal updates are the two external effects.
|
|
97
96
|
confirm: {
|
|
98
|
-
applies: (input) => input.attendance === "no_show"
|
|
99
|
-
|
|
97
|
+
applies: (input) => input.attendance === "no_show" ||
|
|
98
|
+
input.outcome === "won" ||
|
|
99
|
+
input.outcome === "lost",
|
|
100
|
+
description: "attendance is no_show or outcome is won or lost (can message a lead or update a connected CRM deal)",
|
|
100
101
|
},
|
|
101
102
|
inputSchema: {
|
|
102
103
|
...WORKSPACE_INPUT_FIELDS,
|
package/dist/tool.js
CHANGED
|
@@ -91,7 +91,7 @@ function buildFromTaskTool(tool) {
|
|
|
91
91
|
...openApiToolOutputFields({ body: null, untrustedContent }),
|
|
92
92
|
result: tool.outputSchema,
|
|
93
93
|
},
|
|
94
|
-
readOnly: composed.every((operation) => operation.
|
|
94
|
+
readOnly: composed.every((operation) => operation.readOnly),
|
|
95
95
|
// A task tool overwrites or removes existing state when any write it
|
|
96
96
|
// composes does.
|
|
97
97
|
mutatesExisting: composed.some((operation) => operation.mutatesExisting),
|
|
@@ -144,9 +144,8 @@ function toolResult(tool, lucaLayer, surface,
|
|
|
144
144
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- input is the same raw MCP JSON-RPC tool-call arguments registerTool's server.registerTool callback hands in; parseToolInput below is what actually validates it.
|
|
145
145
|
input) {
|
|
146
146
|
const logCall = (outcome, elapsed) =>
|
|
147
|
-
// A defect's own message never lands here: it can
|
|
148
|
-
// text derived from the call's input.
|
|
149
|
-
// tool result.
|
|
147
|
+
// A defect's own message never lands here or in the tool result: it can
|
|
148
|
+
// carry arbitrary thrown text derived from the call's input.
|
|
150
149
|
logAgentSurfaceEvent("mcp.tool.called", {
|
|
151
150
|
toolName: tool.name,
|
|
152
151
|
toolset: surface.toolset,
|
|
@@ -184,12 +183,11 @@ function settle(tool, surface, exit) {
|
|
|
184
183
|
asErrorResult(failure, authChallengeMeta(failure, tool.oauthScope, surface.resourceMetadataUrl)),
|
|
185
184
|
];
|
|
186
185
|
}
|
|
187
|
-
const defect = Cause.squash(cause);
|
|
188
186
|
return [
|
|
189
187
|
"defect",
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
188
|
+
// A defect may include request data or credentials in its message. Keep
|
|
189
|
+
// the protocol error generic and record only the defect outcome in logs.
|
|
190
|
+
asErrorResult("Unexpected Luca MCP tool failure"),
|
|
193
191
|
];
|
|
194
192
|
}
|
|
195
193
|
/** The Effect program that runs an operation, auto-paginating list tools. */
|
package/dist/version.js
CHANGED
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Bump this in the same commit as package.json and server.json.
|
|
11
11
|
*/
|
|
12
|
-
export const LUCA_MCP_VERSION = "0.3.
|
|
12
|
+
export const LUCA_MCP_VERSION = "0.3.3";
|
|
13
13
|
/** The user-agent every outbound Luca API call identifies itself with. */
|
|
14
14
|
export const LUCA_MCP_USER_AGENT = `luca-mcp/${LUCA_MCP_VERSION}`;
|