@setluca/mcp 0.5.0 → 0.5.2

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 CHANGED
@@ -10,6 +10,86 @@ input schema, or changing what a tool does to a workspace.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.5.2] - 2026-10-23
14
+
15
+ ### Fixed
16
+
17
+ - Connections keep working past their first hour. The 401 challenge now asks
18
+ for `offline_access` along with the luca scopes, and the authorization server
19
+ issues a refresh token only when the grant carries it. Claude and ChatGPT ask
20
+ for exactly what the challenge names, so until now neither got a refresh
21
+ token and each connection lapsed with its one-hour access token.
22
+ - claude.ai can connect. `serverInfo` no longer carries `icons`, which makes
23
+ claude.ai abandon the session after `initialize`
24
+ ([anthropics/claude-ai-mcp#474](https://github.com/anthropics/claude-ai-mcp/issues/474)).
25
+ The server card at `/.well-known/mcp/server-card.json` still lists them.
26
+ - `luca_conversations_media_retry` and `luca_review_queue_media_retry` set
27
+ `openWorldHint: true`. A retried transcription sends the voice note to
28
+ OpenRouter's speech-to-text. Neither reaches a lead, so neither needs
29
+ `confirm: true`.
30
+ - `luca_analytics_rollup` returns per-campaign analytics. It looked for
31
+ campaigns under `items`, but the API lists them under `campaigns`, so its
32
+ `campaigns` section had always come back empty.
33
+ - `luca_conversations_send_status`, `luca_bookings_managed_availability`, and
34
+ `luca_webhooks_events_list` require `query`. Each route has a required query
35
+ parameter, so a call without `query` already failed with the API's 400; the
36
+ tool schema now says so. No call that worked before is rejected.
37
+ - `luca_leads_draft` and `luca_draft_reply` set `destructiveHint: true` and
38
+ `openWorldHint: true`. Drafting again replaces the lead's earlier unapproved
39
+ draft from these tools, and the drafter sends the conversation to
40
+ OpenRouter's language model, the same reach that makes a transcription
41
+ retry open-world. Neither sends to the lead, so neither needs
42
+ `confirm: true`.
43
+ - Descriptions that said more than the API does:
44
+ - `luca_campaigns_generate` builds its plan with fixed rules, not an AI
45
+ model;
46
+ - `luca_leads_update` cannot set a score;
47
+ - `luca_reschedule_call`, `luca_bookings_update`, and `luca_bookings_cancel`
48
+ say that a Google Calendar booking's calendar emails the lead, and Cal.com
49
+ may too, where the reschedule tool had said no message is sent.
50
+
51
+ ### Changed
52
+
53
+ - The 401 challenge asks for every scope instead of `luca:read luca:draft`.
54
+ Claude requests what the challenge names, and the consent screen offers only
55
+ what was requested, so a coach could not let Claude send or book. The consent
56
+ screen still starts at read and draft, with every higher scope unticked.
57
+ - Built on Effect 4.0.6, up from 4.0.0. Its JSON Schema output flattens a
58
+ nested `anyOf`, which reshapes `luca_close_call_loop`'s output schema without
59
+ changing what it accepts. No other tool's schema changed.
60
+ - `typecheck` and `build` run the Effect language service
61
+ ([`@effect/tsgo`](https://github.com/Effect-TS/tsgo)) and fail on any of its
62
+ errors, warnings, or suggestions.
63
+
64
+ ## [0.5.1] - 2026-10-22
65
+
66
+ ### Fixed
67
+
68
+ - Tool annotations now match what each tool does, as the October 2026 audit
69
+ for the OpenAI app directory listed them. All 203 tools it covered match its
70
+ corrected values, and the 21 tools added in 0.5.0 follow the same rules.
71
+ - `idempotentHint` is true for 33 writes that a repeat with the same input
72
+ cannot move further, such as `luca_leads_notes_delete`,
73
+ `luca_insights_dismiss`, `luca_broadcasts_cancel`, and
74
+ `luca_pause_cadence`. It was true only for reads before.
75
+ - `openWorldHint` is true for writes the coach's webhook subscribers hear
76
+ about (`luca_leads_create`, `luca_leads_update`,
77
+ `luca_leads_identities_attach`, `luca_review_queue_reject`,
78
+ `luca_review_queue_bulk_reject`, `luca_broadcasts_pause`) and for the call
79
+ summaries that later reach the CRM (`luca_call_events_summary_generate`,
80
+ `luca_call_events_backfill`). None of them needs `confirm: true`.
81
+ - `luca_leads_import_preview` is read-only. It saves nothing.
82
+ - `luca_integrations_crm_connections_schema` is no longer read-only, because
83
+ it refreshes Luca's cached copy of the CRM schema.
84
+ - `luca_review_queue_objection_variant` is no longer destructive. It adds a
85
+ learning event and leaves the draft as it was.
86
+ - `luca_integrations_crm_sync_runs_create` is titled "Preview a CRM sync" and
87
+ no longer asks for `confirm: true` or claims to reach the CRM. The API
88
+ refuses a live sync, so the tool only records a dry-run preview.
89
+ - `luca_reschedule_call` says that a Cal.com or Google Calendar move waits for
90
+ the provider: until it confirms, the booking reads `pending_provider` and
91
+ keeps its old time.
92
+
13
93
  ## [0.5.0] - 2026-10-22
14
94
 
15
95
  ### Breaking
@@ -10,6 +10,7 @@ export function operationAnnotationSource(operation) {
10
10
  title: operation.title,
11
11
  readOnly: operation.readOnly,
12
12
  mutatesExisting: operation.mutatesExisting,
13
+ idempotent: operation.idempotent,
13
14
  ...optionalField("confirm", operation.confirm),
14
15
  openWorld: operation.openWorld,
15
16
  };
@@ -34,11 +35,11 @@ export function toolAnnotations(source) {
34
35
  // existed before the call.
35
36
  destructiveHint: !source.readOnly &&
36
37
  (source.confirm !== undefined || source.mutatesExisting),
37
- // The idempotency key is optional in the MCP input. The HTTP layer mints a
38
- // new key when it is absent, so two otherwise identical writes can still
39
- // have different effects. Only advertise the unconditional read guarantee
40
- // until the key becomes a required tool argument.
41
- idempotentHint: source.readOnly,
38
+ // Stated per operation: every read, plus the writes a repeat cannot move
39
+ // further, such as a delete, a dismiss, or a whole-value replace. It never
40
+ // leans on the idempotency key, which the HTTP layer mints fresh when the
41
+ // caller leaves it out.
42
+ idempotentHint: source.idempotent,
42
43
  // Every call goes to the Luca API, but most only touch the coach's own
43
44
  // workspace. The hint is true only when the call can reach past it: a
44
45
  // lead's inbox, an outside URL, or a connected calendar or CRM.
@@ -1,9 +1,8 @@
1
1
  import * as Arr from "effect/Array";
2
2
  import * as Effect from "effect/Effect";
3
- import * as Option from "effect/Option";
4
3
  import * as R from "effect/Record";
5
- import * as Schema from "effect/Schema";
6
4
  import { LucaApi } from "./http.js";
5
+ import { recordIdsOf } from "./pagination.js";
7
6
  /**
8
7
  * Argument completion for the two ids a coach types by hand. MCP completes
9
8
  * prompt arguments and resource-template variables; there is no tool-argument
@@ -28,27 +27,6 @@ const COMPLETION_SOURCES = {
28
27
  * array it gets back, so a source with more ids than this page cannot say so.
29
28
  */
30
29
  const COMPLETION_PAGE_SIZE = 100;
31
- /**
32
- * Reads a list page down to the ids in one array field, ignoring every other
33
- * field on the page and every other field on a record. A page that arrives in
34
- * any other shape decodes to nothing, which the caller offers as no
35
- * suggestions.
36
- */
37
- function idsDecoder(itemsKey) {
38
- const decode = Schema.decodeUnknownOption(Schema.Struct({
39
- [itemsKey]: Schema.Array(Schema.Struct({ id: Schema.String })),
40
- }));
41
- return (page) => Option.match(decode(page), {
42
- onNone: () => [],
43
- // Read through the values rather than the key: the struct declares
44
- // exactly one field, so a page that decoded carries that field's records
45
- // and nothing else. Indexing by `itemsKey` would say the same thing while
46
- // reading as though the key might be missing.
47
- onSome: (decoded) => R.values(decoded)
48
- .flat()
49
- .map((record) => record.id),
50
- });
51
- }
52
30
  /**
53
31
  * The completion callbacks for one server, closed over the layer that serves
54
32
  * the reads.
@@ -62,14 +40,9 @@ export function completionCallbacks(operationById, lucaLayer) {
62
40
  const operation = operationById(operationId);
63
41
  // A completion source has to be a list route: the ids come out of the
64
42
  // page's own array field, and a route that does not paginate carries none.
65
- // Naming one that cannot serve them is a wiring mistake, so it is refused
66
- // while the server is being built rather than on a keystroke.
67
- const itemsKey = operation.pageContract?.itemsKey;
68
- if (itemsKey === undefined) {
69
- // oxlint-disable-next-line effect/avoid-untagged-errors -- raw failure on an invariant or interop path; not a caller-recoverable domain failure
70
- throw new Error(`Completion source ${operationId} carries no list of records to read ids from`);
71
- }
72
- const idsOn = idsDecoder(itemsKey);
43
+ // `recordIdsOf` refuses any other while the server is being built rather
44
+ // than on a keystroke.
45
+ const idsOn = recordIdsOf(operation);
73
46
  return (value) => Effect.runPromise(Effect.gen(function* () {
74
47
  const api = yield* LucaApi;
75
48
  const page = yield* api.request({
@@ -81,7 +54,7 @@ export function completionCallbacks(operationById, lucaLayer) {
81
54
  // Ids are UUIDs, which print in either case, and a client may type
82
55
  // the capitals it copied from elsewhere.
83
56
  const typed = (value ?? "").toLowerCase();
84
- return idsOn(page).filter((id) => id.toLowerCase().startsWith(typed));
57
+ return idsOn(page, COMPLETION_PAGE_SIZE).filter((id) => id.toLowerCase().startsWith(typed));
85
58
  }).pipe(Effect.provide(lucaLayer),
86
59
  // A failed read answers with no suggestions. A defect is left alone:
87
60
  // that one is our own bug, and an empty list is exactly how it would
package/dist/errors.js CHANGED
@@ -18,7 +18,7 @@ export class LucaDecodeError extends Schema.TaggedError()("LucaDecodeError", {
18
18
  }) {
19
19
  }
20
20
  export class LucaHttpError extends Schema.TaggedError()("LucaHttpError", {
21
- status: Schema.Number,
21
+ status: Schema.Int,
22
22
  statusText: Schema.String,
23
23
  body: Schema.Json,
24
24
  requestId: Schema.optionalKey(Schema.String),
@@ -26,7 +26,7 @@ export class LucaHttpError extends Schema.TaggedError()("LucaHttpError", {
26
26
  * How long the server asked us to wait, from its `Retry-After` header. Absent
27
27
  * when the header was missing or unreadable.
28
28
  */
29
- retryAfterMs: Schema.optionalKey(Schema.Number),
29
+ retryAfterMs: Schema.optionalKey(Schema.Finite),
30
30
  }) {
31
31
  }
32
32
  /**