@setluca/mcp 0.3.0 → 0.3.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,30 @@ input schema, or changing what a tool does to a workspace.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.3.2] - 2026-10-06
14
+
15
+ ### Fixed
16
+
17
+ - Correct MCP safety hints for CRM schema and health probes, knowledge-source
18
+ syncs, and the settings read that creates defaults on first use.
19
+ - Clarify the display names and descriptions of the ghosted-lead analytics and
20
+ weekly business report tools without changing their stable tool names.
21
+
22
+ ## [0.3.1] - 2026-10-06
23
+
24
+ ### Fixed
25
+
26
+ - Restore authenticated remote MCP discovery and tool calls on Cloudflare.
27
+ Token checks and tool requests now use a direct service binding to Luca's API,
28
+ avoiding the public API's WAF challenge. Both paths use Cloudflare's supported
29
+ manual redirect mode and still reject redirected responses without forwarding
30
+ credentials.
31
+
32
+ ### Changed
33
+
34
+ - The remote tool catalog includes explicit safety annotations for directory
35
+ review. The protocol suite checks the published `tools/list` shape.
36
+
13
37
  ## [0.3.0] - 2026-10-05
14
38
 
15
39
  This is the first Luca MCP release from its own repository. The package and
@@ -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.method === "GET",
11
+ readOnly: operation.readOnly,
12
12
  mutatesExisting: operation.mutatesExisting,
13
13
  ...optionalField("confirm", operation.confirm),
14
14
  openWorld: operation.openWorld,
package/dist/http.js CHANGED
@@ -136,17 +136,18 @@ function errorBody(text) {
136
136
  */
137
137
  const describeUrl = (url) => `${url.origin}${url.pathname}`;
138
138
  /** One trip to the API, with no retry and no timeout of its own. */
139
- function sendOnce(config, input, requestIdempotencyKey) {
139
+ function sendOnce(config, input, requestIdempotencyKey, fetchImpl) {
140
140
  return Effect.gen(function* () {
141
141
  const url = yield* buildUrl(config, input);
142
142
  const init = buildRequestInit(config, input, requestIdempotencyKey);
143
143
  const response = yield* Effect.tryPromise({
144
144
  try: (signal) =>
145
145
  // fallow-ignore-next-line security-sink -- the origin is the configured Luca api base url and the path comes from the generated operation manifest with its params encoded, so a tool argument can never move the destination host
146
- fetch(url, {
146
+ fetchImpl(url, {
147
147
  ...init,
148
- // A redirect would carry the API key to a host we never named.
149
- redirect: "error",
148
+ // Cloudflare supports manual redirects, not `error`. A 3xx response
149
+ // falls through as an HTTP error without forwarding the API key.
150
+ redirect: "manual",
150
151
  signal,
151
152
  }),
152
153
  catch: (cause) => new LucaNetworkError({
@@ -190,12 +191,12 @@ function sendOnce(config, input, requestIdempotencyKey) {
190
191
  }));
191
192
  });
192
193
  }
193
- export function createLucaApi(config) {
194
+ export function createLucaApi(config, fetchImpl = fetch) {
194
195
  const timeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
195
196
  return {
196
197
  // The key each attempt carries is minted by `resilient`, which also decides
197
198
  // whether a failure may be replayed under it.
198
- request: (input) => resilient(input, timeoutMs, (key) => sendOnce(config, input, key)),
199
+ request: (input) => resilient(input, timeoutMs, (key) => sendOnce(config, input, key, fetchImpl)),
199
200
  };
200
201
  }
201
202
  export const LucaApiLive = Layer.effect(LucaApi, Effect.gen(function* () {
@@ -36,8 +36,8 @@ export const analyticsOperations = [
36
36
  }),
37
37
  op({
38
38
  id: "analytics.ghostedLeads",
39
- title: "List ghosted leads",
40
- description: "Ghosted-lead counts, rescue-recovery rate, lost-revenue estimate, and top conversation-killer clusters for a period.",
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
  }),
@@ -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({
@@ -24,6 +24,7 @@ export const integrationsOperations = [
24
24
  op({
25
25
  id: "integrations.crm.connections.schema",
26
26
  title: "Read a CRM's field schema",
27
+ openWorld: true,
27
28
  // sampleRecords are literal contact and deal rows from the coach's CRM, which leads and other people filled in.
28
29
  untrustedContent: true,
29
30
  description: "Discover CRM provider schema.",
@@ -31,6 +32,7 @@ export const integrationsOperations = [
31
32
  op({
32
33
  id: "integrations.crm.connections.health",
33
34
  title: "Check a CRM connection's health",
35
+ openWorld: true,
34
36
  description: "Probe CRM provider health.",
35
37
  }),
36
38
  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/remote.js CHANGED
@@ -168,7 +168,7 @@ export function createRemoteResponder(options) {
168
168
  return yield* Effect.fail(invalidToken(options.resourceMetadataUrl));
169
169
  }
170
170
  const handler = createMcpHandler(() => createLucaServer({
171
- lucaLayer: Layer.succeed(LucaApi, createLucaApi(config.value)),
171
+ lucaLayer: Layer.succeed(LucaApi, createLucaApi(config.value, options.apiFetch)),
172
172
  ...optionalField("toolset", options.toolset),
173
173
  ...optionalField("resourceMetadataUrl", options.resourceMetadataUrl),
174
174
  }), { ...optionalField("onerror", options.onError) });
@@ -177,8 +177,8 @@ const getAnalytics = taskTool({
177
177
  });
178
178
  const magicMonday = taskTool({
179
179
  name: "luca_analytics_magic_monday",
180
- title: "Get the Magic Monday report",
181
- description: "Return the coach's weekly business intelligence report: pipeline funnel, revenue and ROI, response-speed impact, a point-in-time revenue forecast, and call intelligence (show/close rates), all for the current week. Composes five analytics reads into a single call. Example: call with {} on Monday morning for the week's report.",
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
  },
@@ -16,8 +16,9 @@ export const sha256Hex = (value) => Effect.tryPromise(() => crypto.subtle.digest
16
16
  * One bounded call to the Luca API that carries a token, with `read` turning
17
17
  * the response into the verdict. Timeout but no retry: this is on the auth
18
18
  * path of every request, and the client retries a 503 on its own schedule. A
19
- * redirect is an error, because it would carry the token to a host we never
20
- * named.
19
+ * redirect is refused, because it would carry the token to a host we never
20
+ * named. Cloudflare Workers does not support `redirect: "error"`, so fetch
21
+ * uses `manual` and we reject 3xx responses before the resolver reads them.
21
22
  */
22
23
  export function callApi(settings, fetchImpl, request, read) {
23
24
  const controller = new AbortController();
@@ -25,11 +26,15 @@ export function callApi(settings, fetchImpl, request, read) {
25
26
  return Effect.tryPromise({
26
27
  try: () => fetchImpl(`${settings.apiBaseUrl}${request.path}`, {
27
28
  ...request.init,
28
- redirect: "error",
29
+ redirect: "manual",
29
30
  signal: controller.signal,
30
31
  }),
31
32
  catch: unavailable("Could not reach token verification"),
32
- }).pipe(Effect.flatMap(read), Effect.timeoutOrElse({
33
+ }).pipe(Effect.flatMap((response) => response.status >= 300 && response.status < 400
34
+ ? Effect.fail(new TokenVerificationUnavailable({
35
+ message: "Token verification redirected",
36
+ }))
37
+ : read(response)), Effect.timeoutOrElse({
33
38
  duration: Duration.millis(timeoutMs),
34
39
  orElse: () => Effect.fail(new TokenVerificationUnavailable({
35
40
  message: `Token verification did not answer within ${timeoutMs}ms`,
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.method === "GET"),
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),
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.0";
12
+ export const LUCA_MCP_VERSION = "0.3.2";
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}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@setluca/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "description": "Let your AI assistant help with Luca conversations, leads, replies, campaigns, and bookings.",
6
6
  "keywords": [