@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.
@@ -5,9 +5,12 @@ import * as Schema from "effect/Schema";
5
5
  import { formatError } from "../errors.js";
6
6
  import { WORKSPACE_INPUT_FIELDS } from "../fields.js";
7
7
  import { logAgentSurfaceEvent } from "../observability.js";
8
- import { isPartialFailure, listingIds, numberInput, operationResult, resultOrError, taskTool, } from "./runtime.js";
8
+ import { operationById } from "../operations.js";
9
+ import { recordIdsOf } from "../pagination.js";
10
+ import { isPartialFailure, numberInput, operationResult, resultOrError, taskTool, } from "./runtime.js";
9
11
  const DEFAULT_QUEUE_LIMIT = 20;
10
12
  const DEFAULT_EXPLAIN_TOP = 3;
13
+ const queueIds = recordIdsOf(operationById("reviewQueue.list"));
11
14
  const TRIAGE_INBOX_TOOL_NAME = "luca_triage_inbox";
12
15
  /**
13
16
  * One explanation, with a failed lookup carried in the result instead of
@@ -79,7 +82,7 @@ const triageInbox = taskTool({
79
82
  const queue = yield* call("reviewQueue.list", {
80
83
  query: { limit: numberInput(input.limit, DEFAULT_QUEUE_LIMIT) },
81
84
  });
82
- const ids = listingIds(queue, numberInput(input.explainTop, DEFAULT_EXPLAIN_TOP));
85
+ const ids = queueIds(queue, numberInput(input.explainTop, DEFAULT_EXPLAIN_TOP));
83
86
  const attempts = yield* Effect.forEach(ids, (id) => explainOne(call, id),
84
87
  // Effect.forEach defaults to concurrency 1 (sequential). `explainTop`
85
88
  // caps this fan-out at 5 independent lookups, so unbounded is fine.
@@ -48,7 +48,7 @@ const flagForHuman = taskTool({
48
48
  const draftReply = taskTool({
49
49
  name: "luca_draft_reply",
50
50
  title: "Draft a reply for a lead",
51
- description: 'Draft a reply for a lead, written the way the coach writes, and put it in the SetLuca review queue, where it waits for the coach to approve it. It never sends, whatever the channel\'s auto-send setting. Optional guidance steers tone or content. Example: call with { leadId, guidance: "keep it short and warm" }.',
51
+ description: "Draft a reply for a lead, written the way the coach writes, and put it in the SetLuca review queue, where it waits for the coach to approve it. Drafting again replaces this lead's earlier unapproved draft from this tool. It never sends, whatever the channel's auto-send setting. Optional guidance steers tone or content. Example: call with { leadId, guidance: \"keep it short and warm\" }.",
52
52
  inputSchema: {
53
53
  ...WORKSPACE_INPUT_FIELDS,
54
54
  leadId: NonEmptyString.annotate({
@@ -63,38 +63,6 @@ export function taskRequest(operation, input, parts = {}) {
63
63
  ...optionalField("workspace", workspaceOverride(input)),
64
64
  };
65
65
  }
66
- /**
67
- * The only part of a listing page a composing tool reads for itself: the ids of
68
- * the items it then fans out over. Decoding that alone leaves the raw page
69
- * intact for the caller, returned exactly as the API sent it, while the ids the
70
- * tool acts on are validated rather than asserted.
71
- */
72
- const ListingIds = Schema.Struct({
73
- items: Schema.optional(Schema.Array(Schema.Struct({ id: Schema.optional(Schema.String) }))),
74
- });
75
- const decodeListingIds = Schema.decodeUnknownOption(ListingIds);
76
- /**
77
- * Ids of the first `limit` items on a listing page. A page whose fields we
78
- * cannot read, or whose items carry no id, contributes nothing. The tool still
79
- * returns the page itself rather than failing over a fan-out it cannot do.
80
- */
81
- export function listingIds(page, limit) {
82
- // Both fallbacks below are empty arrays, and substituting a one-item array
83
- // for either is an equivalent mutation rather than an untested branch: a
84
- // substituted item carries no `id`, so the `filter` at the end of this
85
- // function drops it and the result is the same empty id list. Substituting
86
- // `undefined` for the outer fallback is a real change and is covered by the
87
- // undecodable-page test.
88
- const items = decodeListingIds(page).pipe(
89
- // Stryker disable next-line ArrayDeclaration: a substituted item has no id and is filtered out below
90
- Option.map((decoded) => decoded.items ?? []),
91
- // Stryker disable next-line ArrayDeclaration: a substituted item has no id and is filtered out below
92
- Option.getOrElse(() => []));
93
- return items
94
- .slice(0, limit)
95
- .map((item) => item.id)
96
- .filter((id) => id !== undefined);
97
- }
98
66
  /**
99
67
  * A request body carrying a single text field, or no body at all when the input
100
68
  * is absent or blank. Sending an empty string would mean something different to
package/dist/tool.js CHANGED
@@ -95,6 +95,9 @@ function buildFromTaskTool(tool) {
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),
98
+ // Repeating a task tool has no further effect only when that holds for
99
+ // every operation it composes.
100
+ idempotent: composed.every((operation) => operation.idempotent),
98
101
  ...optionalField("confirm", confirm),
99
102
  untrustedContent,
100
103
  // A task tool reaches as far as the furthest operation it composes.
package/dist/toolsets.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { LUCA_TASK_TOOLS } from "./task-tools.js";
2
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.
3
+ * The tools the `claude` toolset exposes, and a test pins it name for name.
4
+ * Coaches who chose `?toolset=claude` rely on it staying read and draft, so a
5
+ * tool that sends, books, or changes settings does not belong here.
6
6
  */
7
7
  export const CLAUDE_TOOL_NAMES = [
8
8
  "luca_triage_inbox",
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.5.0";
12
+ export const LUCA_MCP_VERSION = "0.5.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.5.0",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Connect SetLuca to Claude, ChatGPT, Cursor, or any MCP client to triage the review queue, look up leads, draft replies, and check campaigns.",
6
6
  "keywords": [
@@ -68,15 +68,17 @@
68
68
  "registry:check": "bun scripts/check-registry.ts",
69
69
  "docs:generate": "bun scripts/generate-docs.ts --write",
70
70
  "docs:check": "bun scripts/generate-docs.ts --check",
71
- "clean": "rm -rf .turbo dist coverage node_modules"
71
+ "clean": "rm -rf .turbo dist coverage node_modules",
72
+ "prepare": "effect-tsgo patch --typescript --no-oxlint"
72
73
  },
73
74
  "dependencies": {
74
75
  "@modelcontextprotocol/server": "^2.0.0",
75
- "effect": "4.0.0"
76
+ "effect": "4.0.6"
76
77
  },
77
78
  "devDependencies": {
78
- "@effect/platform-bun": "4.0.0",
79
- "@effect/vitest": "4.0.0",
79
+ "@effect/platform-bun": "4.0.6",
80
+ "@effect/tsgo": "0.56.0",
81
+ "@effect/vitest": "4.0.6",
80
82
  "@modelcontextprotocol/client": "^2.0.0",
81
83
  "@modelcontextprotocol/inspector": "^2.5.0",
82
84
  "@mpsuesser/oxlint-plugin-effect": "0.6.0",