@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 +24 -0
- package/dist/annotations.js +1 -1
- package/dist/http.js +7 -6
- package/dist/operations/groups/analytics.js +2 -2
- package/dist/operations/groups/insights.js +1 -0
- package/dist/operations/groups/integrations.js +2 -0
- package/dist/operations/groups/knowledge.js +1 -0
- package/dist/operations/registry.js +1 -0
- package/dist/remote.js +1 -1
- package/dist/task-tools/analytics.js +2 -2
- package/dist/token-verification.js +9 -4
- package/dist/tool.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
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,
|
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
|
-
|
|
146
|
+
fetchImpl(url, {
|
|
147
147
|
...init,
|
|
148
|
-
//
|
|
149
|
-
|
|
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: "
|
|
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
|
}),
|
|
@@ -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: "
|
|
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
|
},
|
|
@@ -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
|
|
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: "
|
|
29
|
+
redirect: "manual",
|
|
29
30
|
signal: controller.signal,
|
|
30
31
|
}),
|
|
31
32
|
catch: unavailable("Could not reach token verification"),
|
|
32
|
-
}).pipe(Effect.flatMap(
|
|
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.
|
|
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.
|
|
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}`;
|