@setluca/mcp 0.3.7 → 0.4.0
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 +11 -0
- package/dist/index.js +2 -1
- package/dist/oauth-resolver.js +12 -6
- package/dist/remote.js +3 -1
- package/dist/scopes.js +9 -0
- package/dist/server.js +17 -21
- package/dist/toolsets.js +81 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,17 @@ input schema, or changing what a tool does to a workspace.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.0] - 2026-10-13
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Add a `claude` toolset, selected with `?toolset=claude` or
|
|
18
|
+
`LUCA_TOOLSET=claude`. It lists a fixed set of 43 tools for the Claude
|
|
19
|
+
connector directory and lists only the `luca-api-planner` and
|
|
20
|
+
`luca-morning-report` prompts. For an OAuth connection
|
|
21
|
+
it also hides tools above the grant's capability tier, so a `luca:read` grant
|
|
22
|
+
sees 39. The `full` and `tasks` toolsets are unchanged.
|
|
23
|
+
|
|
13
24
|
## [0.3.7] - 2026-10-13
|
|
14
25
|
|
|
15
26
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
3
|
import { setEventSink, stderrEventSink } from "./observability.js";
|
|
4
|
-
import { createLucaServer
|
|
4
|
+
import { createLucaServer } from "./server.js";
|
|
5
|
+
import { parseToolset } from "./toolsets.js";
|
|
5
6
|
// stdout carries the protocol, so event lines go to stderr.
|
|
6
7
|
setEventSink(stderrEventSink);
|
|
7
8
|
/**
|
package/dist/oauth-resolver.js
CHANGED
|
@@ -4,15 +4,18 @@ import * as Redacted from "effect/Redacted";
|
|
|
4
4
|
import * as Schema from "effect/Schema";
|
|
5
5
|
import { TokenVerificationUnavailable } from "./errors.js";
|
|
6
6
|
import { NonEmptyString } from "./fields.js";
|
|
7
|
+
import { highestCapabilityScope } from "./scopes.js";
|
|
7
8
|
import { callApi, unavailable, } from "./token-verification.js";
|
|
8
9
|
/**
|
|
9
10
|
* The body POST /oauth/resolve answers with for a token it accepted. The API
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* enforces the grant's scopes on every call. This resolver reads them only to
|
|
12
|
+
* learn the capability tier, which the `claude` toolset uses to hide tools the
|
|
13
|
+
* grant cannot call. An empty key is an API fault, not a key,
|
|
12
14
|
* so it fails as unavailable instead of reaching the API as a blank header.
|
|
13
15
|
*/
|
|
14
16
|
const ResolvePayload = Schema.Struct({
|
|
15
17
|
apiKey: Schema.optionalKey(NonEmptyString),
|
|
18
|
+
scopes: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
16
19
|
});
|
|
17
20
|
const decodeResolvePayload = Schema.decodeUnknownEffect(ResolvePayload);
|
|
18
21
|
/**
|
|
@@ -50,10 +53,13 @@ function requestConfig(settings, fetchImpl, token) {
|
|
|
50
53
|
return callApi(settings, fetchImpl, {
|
|
51
54
|
path: "/oauth/resolve",
|
|
52
55
|
init: { method: "POST", headers: { authorization: `Bearer ${token}` } },
|
|
53
|
-
}, resolvePayload).pipe(Effect.map(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
}, resolvePayload).pipe(Effect.map(Option.flatMapNullishOr((body) => body.apiKey === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: {
|
|
59
|
+
...settings,
|
|
60
|
+
apiKey: Redacted.make(body.apiKey),
|
|
61
|
+
grant: highestCapabilityScope(body.scopes ?? []),
|
|
62
|
+
})));
|
|
57
63
|
}
|
|
58
64
|
/** Three base64url segments: the form a JWT access token takes. */
|
|
59
65
|
const JWT_ACCESS_TOKEN = /^[\w-]+\.[\w-]+\.[\w-]+$/;
|
package/dist/remote.js
CHANGED
|
@@ -12,8 +12,9 @@ import { optionalField } from "./optional-field.js";
|
|
|
12
12
|
import { RATE_LIMIT_WINDOW_MS, createLocalRateLimits, } from "./rate-limit.js";
|
|
13
13
|
import { OAUTH_DEFAULT_SCOPE } from "./scopes.js";
|
|
14
14
|
import { toJsonPayload } from "./serialization.js";
|
|
15
|
-
import { createLucaServer
|
|
15
|
+
import { createLucaServer } from "./server.js";
|
|
16
16
|
import { sha256Hex } from "./token-verification.js";
|
|
17
|
+
import { parseToolset } from "./toolsets.js";
|
|
17
18
|
export { bindingRateLimits, createLocalRateLimits } from "./rate-limit.js";
|
|
18
19
|
export { apiKeyResolver, looksLikeDeveloperKey } from "./resolvers.js";
|
|
19
20
|
/**
|
|
@@ -170,6 +171,7 @@ export function createRemoteResponder(options) {
|
|
|
170
171
|
const handler = createMcpHandler(() => createLucaServer({
|
|
171
172
|
lucaLayer: Layer.succeed(LucaApi, createLucaApi(config.value, options.apiFetch)),
|
|
172
173
|
...optionalField("toolset", options.toolset),
|
|
174
|
+
...optionalField("grant", config.value.grant),
|
|
173
175
|
...optionalField("resourceMetadataUrl", options.resourceMetadataUrl),
|
|
174
176
|
}), { ...optionalField("onerror", options.onError) });
|
|
175
177
|
// The handler lives until the response body is done with, then
|
package/dist/scopes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Arr from "effect/Array";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
2
3
|
import * as Order from "effect/Order";
|
|
3
4
|
/**
|
|
4
5
|
* The OAuth scopes this server advertises, and the API-key scopes each one
|
|
@@ -147,6 +148,14 @@ export function tierGrants(scope, apiScope) {
|
|
|
147
148
|
export function capabilityRank(scope) {
|
|
148
149
|
return CAPABILITY_LADDER.indexOf(scope);
|
|
149
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The highest capability scope in a grant's scope list. A list naming none
|
|
153
|
+
* (only `offline_access`, say, or an empty list) resolves to `luca:read`, the
|
|
154
|
+
* lowest, so an unreadable grant never lists more than a read grant would.
|
|
155
|
+
*/
|
|
156
|
+
export function highestCapabilityScope(scopes) {
|
|
157
|
+
return Arr.findLast(CAPABILITY_LADDER, (scope) => Arr.contains(scopes, scope)).pipe(Option.getOrElse(() => "luca:read"));
|
|
158
|
+
}
|
|
150
159
|
/**
|
|
151
160
|
* The lowest capability scope that grants `apiScope`, or `undefined` when no
|
|
152
161
|
* tier grants it. Reading the ladder from the bottom is what makes the answer
|
package/dist/server.js
CHANGED
|
@@ -16,12 +16,13 @@ import { optionalField } from "./optional-field.js";
|
|
|
16
16
|
import { LUCA_API_PLANNER_PROMPT, LUCA_PROMPTS, } from "./prompts.js";
|
|
17
17
|
import { provenanceFields } from "./provenance.js";
|
|
18
18
|
import { LUCA_OPERATION_MANIFEST_RESOURCE, LUCA_RESOURCES, } from "./resources.js";
|
|
19
|
-
import { oauthScopeForOperation } from "./scopes.js";
|
|
19
|
+
import { capabilityRank, oauthScopeForOperation, } from "./scopes.js";
|
|
20
20
|
import { toJsonText } from "./serialization.js";
|
|
21
21
|
import { toolSchema } from "./standard-schema.js";
|
|
22
22
|
import { LUCA_TASK_TOOLS } from "./task-tools.js";
|
|
23
23
|
import { buildCache } from "./tool-cache.js";
|
|
24
24
|
import { fromOperation, fromTaskTool, operationProgram, registerTool, } from "./tool.js";
|
|
25
|
+
import { TOOLSETS } from "./toolsets.js";
|
|
25
26
|
import { LUCA_MCP_VERSION } from "./version.js";
|
|
26
27
|
const LucaLive = LucaApiLive.pipe(Layer.provide(LucaConfigLive));
|
|
27
28
|
/**
|
|
@@ -94,11 +95,6 @@ const LUCA_CACHE_HINTS = {
|
|
|
94
95
|
},
|
|
95
96
|
"server/discover": { ttlMs: STATIC_LIST_TTL_MS, cacheScope: "private" },
|
|
96
97
|
};
|
|
97
|
-
// Only "tasks" selects the trimmed surface; every other value, including an
|
|
98
|
-
// unset query param, takes the full-surface branch.
|
|
99
|
-
export function parseToolset(value) {
|
|
100
|
-
return value === "tasks" ? "tasks" : "full";
|
|
101
|
-
}
|
|
102
98
|
function registerLucaResources(server, lucaLayer, completions, resourceMetadataUrl) {
|
|
103
99
|
Arr.forEach(LUCA_RESOURCES, (resource) => {
|
|
104
100
|
const operation = operationById(resource.operationId);
|
|
@@ -299,16 +295,14 @@ function registerPrompt(server, prompt, completions) {
|
|
|
299
295
|
* Build the Luca MCP server: registers the operation tools, the task tools, the
|
|
300
296
|
* `luca://…` resources, and the planner prompt against a shared `LucaApi` layer.
|
|
301
297
|
* `options.lucaLayer` swaps the HTTP client (tests inject an in-memory handler);
|
|
302
|
-
* `options.toolset` picks the tool set
|
|
303
|
-
*
|
|
298
|
+
* `options.toolset` picks the tool set from `TOOLSETS`; `"full"`, the default,
|
|
299
|
+
* exposes everything. `options.grant` trims the `claude` set to the tools the
|
|
300
|
+
* connection's OAuth grant can call.
|
|
304
301
|
*/
|
|
305
302
|
export function createLucaServer(options = {}) {
|
|
306
303
|
const lucaLayer = options.lucaLayer ?? LucaLive;
|
|
307
|
-
// The default value is only ever compared against `"tasks"` below; any other
|
|
308
|
-
// string (including `""`) takes the full-surface branch, so the literal here
|
|
309
|
-
// cannot change behavior.
|
|
310
|
-
// Stryker disable next-line StringLiteral: only `"tasks"` is compared downstream, so any non-tasks default behaves as full
|
|
311
304
|
const toolset = options.toolset ?? "full";
|
|
305
|
+
const definition = TOOLSETS[toolset];
|
|
312
306
|
const surface = {
|
|
313
307
|
toolset,
|
|
314
308
|
...optionalField("resourceMetadataUrl", options.resourceMetadataUrl),
|
|
@@ -317,18 +311,20 @@ export function createLucaServer(options = {}) {
|
|
|
317
311
|
cacheHints: LUCA_CACHE_HINTS,
|
|
318
312
|
instructions: LUCA_SERVER_INSTRUCTIONS,
|
|
319
313
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
Arr.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
314
|
+
const listed = (tool) => !definition.grantFiltered ||
|
|
315
|
+
options.grant === undefined ||
|
|
316
|
+
capabilityRank(tool.oauthScope) <= capabilityRank(options.grant);
|
|
317
|
+
const inToolset = (name) => definition.tools === undefined || Arr.contains(definition.tools, name);
|
|
318
|
+
const tools = [
|
|
319
|
+
...LUCA_OPERATIONS.filter((operation) => inToolset(operation.toolName)).map(fromOperation),
|
|
320
|
+
...LUCA_TASK_TOOLS.filter((tool) => inToolset(tool.name)).map(fromTaskTool),
|
|
321
|
+
].filter(listed);
|
|
322
|
+
Arr.forEach(tools, (tool) => {
|
|
323
|
+
registerTool(server, tool, lucaLayer, surface);
|
|
328
324
|
});
|
|
329
325
|
registerOperationManifestResource(server);
|
|
330
326
|
const completions = completionCallbacks(operationById, lucaLayer);
|
|
331
327
|
registerLucaResources(server, lucaLayer, completions, options.resourceMetadataUrl);
|
|
332
|
-
registerPrompts(server, LUCA_PROMPTS, completions);
|
|
328
|
+
registerPrompts(server, LUCA_PROMPTS.filter((prompt) => !Arr.contains(definition.hiddenPrompts, prompt.name)), completions);
|
|
333
329
|
return server;
|
|
334
330
|
}
|
package/dist/toolsets.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { LUCA_TASK_TOOLS } from "./task-tools.js";
|
|
2
|
+
/**
|
|
3
|
+
* The tools the `claude` toolset exposes. This list is the contract with the
|
|
4
|
+
* connector directory submission, and a test pins it name for name. Adding or
|
|
5
|
+
* removing a tool here changes what the directory lists.
|
|
6
|
+
*/
|
|
7
|
+
export const CLAUDE_TOOL_NAMES = [
|
|
8
|
+
"luca_triage_inbox",
|
|
9
|
+
"luca_morning_report",
|
|
10
|
+
"luca_find_leads",
|
|
11
|
+
"luca_post_call_queue",
|
|
12
|
+
"luca_analytics_rollup",
|
|
13
|
+
"luca_analytics_magic_monday",
|
|
14
|
+
"luca_analytics_deep_dive",
|
|
15
|
+
"luca_capabilities_get",
|
|
16
|
+
"luca_leads_list",
|
|
17
|
+
"luca_leads_get",
|
|
18
|
+
"luca_leads_explain",
|
|
19
|
+
"luca_conversations_list",
|
|
20
|
+
"luca_conversations_get",
|
|
21
|
+
"luca_conversations_messages",
|
|
22
|
+
"luca_review_queue_list",
|
|
23
|
+
"luca_review_queue_explain",
|
|
24
|
+
"luca_review_queue_sla",
|
|
25
|
+
"luca_bookings_list",
|
|
26
|
+
"luca_bookings_get",
|
|
27
|
+
"luca_bookings_availability",
|
|
28
|
+
"luca_bookings_brief",
|
|
29
|
+
"luca_bookings_needs_outcome",
|
|
30
|
+
"luca_call_events_needing_feedback",
|
|
31
|
+
"luca_call_events_get",
|
|
32
|
+
"luca_channels_list",
|
|
33
|
+
"luca_channels_catalog",
|
|
34
|
+
"luca_reports_scorecard",
|
|
35
|
+
"luca_reports_morning",
|
|
36
|
+
"luca_campaigns_list",
|
|
37
|
+
"luca_campaigns_get",
|
|
38
|
+
"luca_campaigns_analytics",
|
|
39
|
+
"luca_broadcasts_list",
|
|
40
|
+
"luca_broadcasts_get",
|
|
41
|
+
"luca_voice_profile",
|
|
42
|
+
"luca_analytics_funnel",
|
|
43
|
+
"luca_analytics_ghosted_leads",
|
|
44
|
+
"luca_analytics_objections",
|
|
45
|
+
"luca_analytics_insights",
|
|
46
|
+
"luca_memory_list",
|
|
47
|
+
"luca_draft_reply",
|
|
48
|
+
"luca_flag_for_human",
|
|
49
|
+
"luca_leads_notes_add",
|
|
50
|
+
"luca_leads_update",
|
|
51
|
+
];
|
|
52
|
+
export const TOOLSETS = {
|
|
53
|
+
full: { hiddenPrompts: [], grantFiltered: false },
|
|
54
|
+
tasks: {
|
|
55
|
+
tools: [
|
|
56
|
+
"luca_capabilities_get",
|
|
57
|
+
...LUCA_TASK_TOOLS.map((tool) => tool.name),
|
|
58
|
+
],
|
|
59
|
+
hiddenPrompts: [],
|
|
60
|
+
grantFiltered: false,
|
|
61
|
+
},
|
|
62
|
+
claude: {
|
|
63
|
+
tools: CLAUDE_TOOL_NAMES,
|
|
64
|
+
hiddenPrompts: [
|
|
65
|
+
"luca-rescue-silent-leads",
|
|
66
|
+
"luca-plan-campaign",
|
|
67
|
+
"luca-plan-broadcast",
|
|
68
|
+
"luca-close-the-call-loop",
|
|
69
|
+
// These name approve-and-send, reject, or learning tools the set leaves out.
|
|
70
|
+
"luca-triage-queue",
|
|
71
|
+
"luca-draft-reply",
|
|
72
|
+
"luca-weekly-review",
|
|
73
|
+
],
|
|
74
|
+
grantFiltered: true,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
// Only "tasks" and "claude" select a trimmed surface; every other value,
|
|
78
|
+
// including an unset query param, takes the full-surface branch.
|
|
79
|
+
export function parseToolset(value) {
|
|
80
|
+
return value === "tasks" || value === "claude" ? value : "full";
|
|
81
|
+
}
|
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.
|
|
12
|
+
export const LUCA_MCP_VERSION = "0.4.0";
|
|
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}`;
|