@setluca/mcp 0.5.1 → 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,57 @@ 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
+
13
64
  ## [0.5.1] - 2026-10-22
14
65
 
15
66
  ### Fixed
@@ -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
  /**