lace-mcp 0.0.2-alpha.19 → 0.0.2-alpha.20

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.
Files changed (3) hide show
  1. package/SKILL.md +2 -2
  2. package/dist/cli.js +183 -82
  3. package/package.json +1 -1
package/SKILL.md CHANGED
@@ -12,8 +12,8 @@ Lace surfaces adoption insights as actionable decisions. When a decision is appr
12
12
  1. Call the `search_decisions` MCP tool with no params to see recently approved decisions, or pass filters such as `query`, `canvasId`, `projectId`, `appName`, or `elementRole` when the user has given scope.
13
13
  2. If the workspace has many canvases or projects, call `list_canvases` or `list_projects` first and use the returned IDs/titles to scope `search_decisions`.
14
14
  3. If there is exactly one relevant decision, proceed with it. If there are multiple plausible matches, ask the user which one to implement.
15
- 4. Call `apply_decision` with the chosen decision ID. Use `target: "code"` for code changes, `target: "slideshow"` for slide content, `target: "graphic"` for graphic specs, or `target: "prototype"` for prototype data.
16
- 5. Use `include: "implementation"` by default. Use `include: "image"` for a cropped element image, `include: "context"` for thread rationale, or `include: "all"` when you need screenshots, cropped image, and rationale together.
15
+ 4. Call `apply_decision` with the chosen decision ID.
16
+ 5. Use `include: "instructions"` by default. Use `include: "crop"` for a cropped element image, `include: "discussion"` for thread rationale, or `include: "all"` when you need screenshots, cropped image, and rationale together.
17
17
  6. Follow the implementation instructions returned by `apply_decision` exactly — they include screenshots, element targeting, and constraints.
18
18
  7. Implement the minimal code change described. Do not refactor or modify anything outside the target element.
19
19
 
package/dist/cli.js CHANGED
@@ -82166,12 +82166,12 @@ var init_zod2 = __esm({
82166
82166
 
82167
82167
  // ../../libs/shared/src/decisionCanvas/types.ts
82168
82168
  function isFullPageBbox(target) {
82169
- return target.bbox_x1 <= 0 && target.bbox_y1 <= 0 && target.bbox_x2 >= 1 && target.bbox_y2 >= 1;
82169
+ return target.bbox_x1 <= FULL_PAGE_EPSILON && target.bbox_y1 <= FULL_PAGE_EPSILON && target.bbox_x2 >= 1 - FULL_PAGE_EPSILON && target.bbox_y2 >= 1 - FULL_PAGE_EPSILON;
82170
82170
  }
82171
82171
  function isCanvasDecisionAnnotation(annotation) {
82172
82172
  return annotation.kind == null || annotation.kind === CANVAS_ANNOTATION_KIND_DECISION;
82173
82173
  }
82174
- var CANVAS_ANNOTATION_KIND_ELEMENT, CANVAS_ANNOTATION_KIND_DECISION, CANVAS_ANNOTATION_KIND_VALUES, CANVAS_ANNOTATION_STATUS_VALUES, CANVAS_ANNOTATION_ANCHOR_STATE_VALUES, CANVAS_ANNOTATION_ANCHOR_SOURCE_VALUES, CANVAS_ANNOTATION_HEADLINE_MAX_LENGTH, uuidSchema, optionalCanvasTextSchema, canvasBboxCoordinateSchema, optionalIsoDateSchema, createCanvasAnnotationInputSchema, CANVAS_ANNOTATION_CHANGE_EVENT_TYPES, CANVAS_PAGE_CHANGE_EVENT_TYPES, CANVAS_REPLY_CHANGE_EVENT_TYPES, CANVAS_THREAD_CHANGE_EVENT_TYPES, CANVAS_CHANGE_EVENT_TYPES, LACE_MCP_INSTRUCTIONS;
82174
+ var CANVAS_ANNOTATION_KIND_ELEMENT, CANVAS_ANNOTATION_KIND_DECISION, CANVAS_ANNOTATION_KIND_VALUES, CANVAS_ANNOTATION_STATUS_VALUES, CANVAS_ANNOTATION_ANCHOR_STATE_VALUES, CANVAS_ANNOTATION_ANCHOR_SOURCE_VALUES, CANVAS_ANNOTATION_HEADLINE_MAX_LENGTH, uuidSchema, optionalCanvasTextSchema, canvasBboxCoordinateSchema, optionalIsoDateSchema, createCanvasAnnotationInputSchema, FULL_PAGE_EPSILON, CANVAS_ANNOTATION_CHANGE_EVENT_TYPES, CANVAS_PAGE_CHANGE_EVENT_TYPES, CANVAS_REPLY_CHANGE_EVENT_TYPES, CANVAS_THREAD_CHANGE_EVENT_TYPES, CANVAS_CHANGE_EVENT_TYPES, LACE_MCP_INSTRUCTIONS;
82175
82175
  var init_types4 = __esm({
82176
82176
  "../../libs/shared/src/decisionCanvas/types.ts"() {
82177
82177
  "use strict";
@@ -82225,6 +82225,7 @@ var init_types4 = __esm({
82225
82225
  if (status === "resolved") return value.resolved_at !== null && value.resolved_at !== void 0;
82226
82226
  return value.resolved_at === null || value.resolved_at === void 0;
82227
82227
  });
82228
+ FULL_PAGE_EPSILON = 0.01;
82228
82229
  CANVAS_ANNOTATION_CHANGE_EVENT_TYPES = [
82229
82230
  "annotation-created",
82230
82231
  "annotation-updated",
@@ -82252,9 +82253,9 @@ Discovery:
82252
82253
  - list_projects: browse projects to find project IDs for scoping.
82253
82254
 
82254
82255
  Action:
82255
- - apply_decision: get the implementation kit for a decision. Set target to control output format (code, slideshow, graphic, prototype). Set include to control detail level (implementation, image, context, all).
82256
+ - apply_decision: get the implementation kit for a decision. Set include to control detail level (instructions, crop, discussion, all).
82256
82257
 
82257
- Workflow: search_decisions -> pick a decision -> apply_decision with the right target.
82258
+ Workflow: search_decisions -> pick a decision -> apply_decision with the desired include mode.
82258
82259
 
82259
82260
  All tools scope to canvases you have access to. Some canvases may be view-only (title and status visible, no decisions). Decision IDs from search_decisions are valid for apply_decision.`;
82260
82261
  }
@@ -82323,18 +82324,21 @@ function deriveFlowContext(decision, allDecisions) {
82323
82324
  if (order === -1) return null;
82324
82325
  return { id: decision.canvas_id, order: order + 1, total: sorted.length };
82325
82326
  }
82327
+ function normalizeRole(role) {
82328
+ return role.startsWith("AX") ? role.slice(2).toLowerCase() : role.toLowerCase();
82329
+ }
82326
82330
  function buildCanvasVocabulary(elementAnnotations) {
82327
82331
  if (elementAnnotations.length === 0) return null;
82328
82332
  const elements = elementAnnotations.map((annotation) => {
82329
82333
  const parts = [];
82330
- if (annotation.element_role) parts.push(annotation.element_role);
82334
+ if (annotation.element_role) parts.push(normalizeRole(annotation.element_role));
82331
82335
  parts.push(annotation.element_visible_text ?? annotation.headline);
82332
82336
  return parts.join(" ");
82333
82337
  }).filter(Boolean).slice(0, MAX_CANVAS_VOCABULARY_ELEMENTS);
82334
82338
  if (elements.length === 0) return null;
82335
- return `<vocabulary>
82336
- ${elements.map((element) => ` <element>${esc3(element)}</element>`).join("\n")}
82337
- </vocabulary>`;
82339
+ return `<page-elements>
82340
+ ${elements.map((element) => ` <item>${esc3(element)}</item>`).join("\n")}
82341
+ </page-elements>`;
82338
82342
  }
82339
82343
  function canvasAnnotationToDecisionXmlInput(decision, allDecisions) {
82340
82344
  return {
@@ -82345,7 +82349,12 @@ function canvasAnnotationToDecisionXmlInput(decision, allDecisions) {
82345
82349
  createdAt: decision.created_at,
82346
82350
  resolvedAt: decision.resolved_at,
82347
82351
  resolvedBy: decision.approved_by ?? null,
82348
- bbox: decision.anchor_state === "detached" || isFullPageBbox(decision) ? null : [decision.bbox_x1, decision.bbox_y1, decision.bbox_x2, decision.bbox_y2],
82352
+ bbox: decision.anchor_state === "detached" || isFullPageBbox(decision) ? null : [
82353
+ Math.round(decision.bbox_x1 * 100),
82354
+ Math.round(decision.bbox_y1 * 100),
82355
+ Math.round(decision.bbox_x2 * 100),
82356
+ Math.round(decision.bbox_y2 * 100)
82357
+ ],
82349
82358
  element: {
82350
82359
  role: decision.element_role,
82351
82360
  label: decision.element_visible_text,
@@ -82374,19 +82383,16 @@ function serializeDecisionXml(decision) {
82374
82383
  const actionsXml = decision.element.actions?.length ? `
82375
82384
  <actions>${esc3(decision.element.actions.join(","))}</actions>` : "";
82376
82385
  return [
82377
- `<decision${attr("id", decision.id)}${attr("status", decision.status)}${optionalAttr("anchor_state", anchorState)}${optionalAttr("element_eid", decision.elementEid)}${attr("created_at", decision.createdAt)}${optionalAttr("resolved_at", decision.resolvedAt)}${optionalAttr("resolved_by", decision.resolvedBy)}>`,
82386
+ `<review-item${attr("status", decision.status)}${optionalAttr("anchor_state", anchorState)}${attr("created_at", decision.createdAt)}${optionalAttr("resolved_at", decision.resolvedAt)}>`,
82378
82387
  decision.bbox ? ` <bbox${attr("x1", decision.bbox[0])}${attr("y1", decision.bbox[1])}${attr("x2", decision.bbox[2])}${attr("y2", decision.bbox[3])} />` : "",
82379
- ` <element${optionalAttr("role", decision.element.role)}${optionalAttr("label", decision.element.label)}${optionalAttr("confidence", decision.element.confidence)}${optionalAttr("interactivity", decision.element.interactivity)}${optionalAttr("type", decision.element.type)}${optionalAttr("value", decision.element.value)}>${actionsXml}`,
82388
+ ` <element${optionalAttr("role", decision.element.role)}${optionalAttr("label", decision.element.label)}${optionalAttr("type", decision.element.type)}${optionalAttr("value", decision.element.value)}>${actionsXml}`,
82380
82389
  " </element>",
82381
82390
  " <content>",
82382
82391
  ` <headline>${esc3(decision.content.headline)}</headline>`,
82383
82392
  decision.content.body ? ` <body>${esc3(decision.content.body)}</body>` : "",
82384
82393
  " </content>",
82385
82394
  ` <screen${optionalAttr("app", decision.screen.appName)}${optionalAttr("window", decision.screen.windowTitle)}${optionalAttr("url", decision.screen.url)}${optionalAttr("viewport_w", decision.screen.viewportWidth)}${optionalAttr("viewport_h", decision.screen.viewportHeight)}${optionalAttr("captured_at", decision.screen.capturedAt)} />`,
82386
- ` <canvas${attr("id", decision.canvas.id)}${attr("page", decision.canvas.pageIndex)} />`,
82387
- decision.thread.id ? ` <thread${attr("id", decision.thread.id)} />` : "",
82388
- decision.flow ? ` <flow${attr("id", decision.flow.id)}${attr("order", decision.flow.order)}${attr("total", decision.flow.total)} />` : "",
82389
- "</decision>"
82395
+ "</review-item>"
82390
82396
  ].filter(Boolean).join("\n");
82391
82397
  }
82392
82398
  function formatApplyDecisionText(decision, allDecisions, vocabulary, threadContext) {
@@ -82405,7 +82411,7 @@ ${messages.join("\n")}
82405
82411
  </thread>`);
82406
82412
  }
82407
82413
  parts.push(
82408
- "Apply the resolved Decision Canvas change. Use the element context, bbox, and screen metadata above as implementation grounding."
82414
+ "Implement the UI change described above. The XML metadata identifies the target element and its position on screen \u2014 use it for grounding only. DO NOT reproduce XML tags, UUIDs, coordinate values, or internal field names in your output. DO: describe the change in natural language or code referencing the element by its visible text, role, or position. DON'T: mention review-item, bbox, page-elements, anchor_state, or any attribute names from the metadata."
82409
82415
  );
82410
82416
  return parts.join("\n\n");
82411
82417
  }
@@ -82470,7 +82476,8 @@ function loaderErrorResponse(error51, fallback) {
82470
82476
  if (error51 instanceof DecisionLoaderError) {
82471
82477
  return { content: [{ type: "text", text: error51.contentText }], isError: true };
82472
82478
  }
82473
- return { content: [{ type: "text", text: fallback }], isError: true };
82479
+ const detail = error51 instanceof Error ? error51.message : String(error51);
82480
+ return { content: [{ type: "text", text: `${fallback} (${detail})` }], isError: true };
82474
82481
  }
82475
82482
  function loaderScope(scope) {
82476
82483
  return { orgId: scope.orgId, userId: scope.userId };
@@ -82502,7 +82509,13 @@ function resolveCropSource(detail) {
82502
82509
  }
82503
82510
  function decisionBbox(decision) {
82504
82511
  if (!isValidBbox(decision)) return null;
82505
- return [decision.bbox_x1, decision.bbox_y1, decision.bbox_x2, decision.bbox_y2];
82512
+ const pad = 0.02;
82513
+ return [
82514
+ Math.max(0, decision.bbox_x1 - pad),
82515
+ Math.max(0, decision.bbox_y1 - pad),
82516
+ Math.min(1, decision.bbox_x2 + pad),
82517
+ Math.min(1, decision.bbox_y2 + pad)
82518
+ ];
82506
82519
  }
82507
82520
  function decisionNotFoundResult(id) {
82508
82521
  return {
@@ -82511,16 +82524,29 @@ function decisionNotFoundResult(id) {
82511
82524
  };
82512
82525
  }
82513
82526
  async function appendImplementationContent(context2, include) {
82514
- const detail = await context2.loader.loadDecisionDetail(context2.id, context2.scope);
82527
+ const [detail, threadContext] = await (include === "all" ? Promise.all([
82528
+ context2.loader.loadDecisionDetail(context2.id, context2.scope),
82529
+ context2.loader.loadThreadContext(context2.id, context2.scope)
82530
+ ]) : [await context2.loader.loadDecisionDetail(context2.id, context2.scope), void 0]);
82515
82531
  if (!detail) return null;
82516
- context2.content.push(...detail.images);
82517
- const vocabulary = await context2.loader.loadElementVocabulary(detail.decision.canvas_id, context2.scope);
82532
+ const vocabulary = await context2.loader.loadElementVocabulary(
82533
+ detail.decision.canvas_id,
82534
+ context2.scope,
82535
+ detail.decision.page_id
82536
+ );
82518
82537
  const vocabStr = vocabulary.length > 0 ? buildCanvasVocabulary(vocabulary) : null;
82519
- const threadContext = include === "all" ? await context2.loader.loadThreadContext(context2.id, context2.scope) : void 0;
82520
82538
  context2.content.push({
82521
82539
  type: "text",
82522
82540
  text: formatApplyDecisionText(detail.decision, detail.allDecisions, vocabStr, threadContext)
82523
82541
  });
82542
+ context2.content.push(...detail.images);
82543
+ const totalImageBytes = detail.images.reduce((sum, img) => sum + (img.type === "image" ? img.data.length : 0), 0);
82544
+ if (totalImageBytes > 5e6) {
82545
+ context2.content.unshift({
82546
+ type: "text",
82547
+ text: "Warning: Large screenshot payload (" + Math.round(totalImageBytes / 1e6) + 'MB base64). Consider using include="instructions" without images if context window is limited.'
82548
+ });
82549
+ }
82524
82550
  return detail;
82525
82551
  }
82526
82552
  async function appendImageContent(context2, existingDetail) {
@@ -82528,7 +82554,12 @@ async function appendImageContent(context2, existingDetail) {
82528
82554
  if (!detail) return null;
82529
82555
  const cropSource = resolveCropSource(detail);
82530
82556
  const bbox = cropSource ? decisionBbox(cropSource) : null;
82531
- if (!cropSource || !bbox) return detail;
82557
+ if (!cropSource || !bbox) {
82558
+ if (detail.images.length > 0 && !context2.content.some((c) => c.type === "image")) {
82559
+ context2.content.push(...detail.images);
82560
+ }
82561
+ return detail;
82562
+ }
82532
82563
  const cropped = await context2.loader.cropElement(cropSource.page.artifact_id, bbox, context2.scope);
82533
82564
  if (cropped) context2.content.push({ type: "image", data: cropped, mimeType: "image/png" });
82534
82565
  return detail;
@@ -82539,61 +82570,57 @@ async function appendContextContent(content, loader, id, scope) {
82539
82570
  content.push({ type: "text", text: "No thread context available for this decision." });
82540
82571
  return;
82541
82572
  }
82542
- const formatted = messages.map((message) => "role" in message ? `[${message.role}] ${message.visibleText ?? ""}` : message.text).join("\n");
82543
- content.push({ type: "text", text: formatted });
82573
+ const formatted = messages.map(
82574
+ (message) => "role" in message ? `<message role="${message.role}">${esc3(message.visibleText ?? "")}</message>` : `<message>${esc3(message.text)}</message>`
82575
+ ).join("\n");
82576
+ content.push({ type: "text", text: `<thread>
82577
+ ${formatted}
82578
+ </thread>` });
82544
82579
  }
82545
82580
  async function handleApplyDecision(loader, id, include, scope) {
82546
82581
  const content = [];
82547
82582
  const context2 = { content, loader, id, scope };
82548
82583
  let detail = null;
82549
- if (include === "implementation" || include === "all") {
82584
+ if (include === "instructions" || include === "all") {
82550
82585
  detail = await appendImplementationContent(context2, include);
82551
82586
  if (!detail) return decisionNotFoundResult(id);
82552
82587
  }
82553
- if (include === "image" || include === "all") {
82588
+ if (include === "crop" || include === "all") {
82554
82589
  detail = await appendImageContent(context2, detail);
82555
82590
  }
82556
- if (include === "context" && content.length === 0) {
82591
+ if (include === "discussion" && content.length === 0) {
82557
82592
  await appendContextContent(content, loader, id, scope);
82558
82593
  }
82559
- return content.length > 0 ? { content } : decisionNotFoundResult(id);
82594
+ if (content.length === 0) {
82595
+ if (detail) {
82596
+ return {
82597
+ content: [
82598
+ {
82599
+ type: "text",
82600
+ text: 'Decision found but no content could be loaded for the requested include mode. Try include="instructions" for the full context.'
82601
+ }
82602
+ ],
82603
+ isError: true
82604
+ };
82605
+ }
82606
+ return decisionNotFoundResult(id);
82607
+ }
82608
+ return { content };
82560
82609
  }
82561
- function registerSearchDecisionsTool(server2, loader, getScope, onToolInvoked) {
82610
+ function registerSearchDecisionsTool({
82611
+ server: server2,
82612
+ loader,
82613
+ getScope,
82614
+ onToolInvoked,
82615
+ onToolError
82616
+ }) {
82562
82617
  server2.registerTool(
82563
82618
  "search_decisions",
82564
82619
  {
82565
82620
  title: "Search Decisions",
82566
- description: "Search and filter resolved decisions with cursor pagination. With no parameters, returns the top 20 ranked by recency and detail. For full detail on a specific decision, use apply_decision.",
82621
+ description: "Search and filter resolved decisions with cursor pagination. With no parameters, returns the top 20 ranked by recency and detail. For full detail on a specific decision, use apply_decision. Tip: start broad (no filters), then narrow with canvasTitle, appName, or elementRole.",
82567
82622
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
82568
- inputSchema: {
82569
- query: external_exports.string().optional().describe("Text search across headline, body, and element text"),
82570
- status: external_exports.enum(["resolved", "suggested", "all"]).default("resolved").optional().describe("Filter by annotation status (default: resolved)"),
82571
- limit: external_exports.number().int().min(1).max(100).default(20).optional().describe("Results per page (1-100, default 20)"),
82572
- cursor: external_exports.string().optional().describe("Opaque pagination cursor from a previous response"),
82573
- sort: external_exports.enum(["relevance", "resolved_at_desc", "created_at_desc"]).default("resolved_at_desc").optional().describe("Sort order"),
82574
- canvasId: external_exports.string().uuid().optional().describe("Exact canvas ID"),
82575
- canvasTitle: external_exports.string().optional().describe("Canvas title substring match"),
82576
- canvasStatus: external_exports.enum(["active", "archived"]).optional().describe("Canvas lifecycle status"),
82577
- pageId: external_exports.string().uuid().optional().describe("Exact page ID"),
82578
- pageName: external_exports.string().optional().describe("Page name substring match"),
82579
- projectId: external_exports.string().uuid().optional().describe("Exact project ID"),
82580
- projectName: external_exports.string().optional().describe("Project name substring match"),
82581
- threadId: external_exports.string().uuid().optional(),
82582
- appName: external_exports.string().optional().describe("App name substring match"),
82583
- windowTitle: external_exports.string().optional().describe("Window title substring match"),
82584
- url: external_exports.string().optional().describe("URL substring match"),
82585
- anchorState: external_exports.enum(["anchored", "reanchored", "detached"]).optional(),
82586
- elementEid: external_exports.string().optional().describe("Exact element accessibility ID"),
82587
- elementRole: external_exports.string().optional().describe("Exact element ARIA role"),
82588
- elementInteractivity: external_exports.boolean().optional(),
82589
- minEndorsements: external_exports.number().int().min(0).optional().describe("Minimum endorsement count"),
82590
- hasReplies: external_exports.boolean().optional().describe("Filter to decisions with replies"),
82591
- createdBy: external_exports.string().uuid().optional().describe("Creator user ID"),
82592
- resolvedFrom: external_exports.string().optional().describe("ISO date lower bound for resolved_at"),
82593
- resolvedTo: external_exports.string().optional().describe("ISO date upper bound for resolved_at"),
82594
- createdFrom: external_exports.string().optional().describe("ISO date lower bound for created_at"),
82595
- createdTo: external_exports.string().optional().describe("ISO date upper bound for created_at")
82596
- }
82623
+ inputSchema: searchDecisionsSchema
82597
82624
  },
82598
82625
  async (params) => {
82599
82626
  onToolInvoked?.("search_decisions");
@@ -82607,38 +82634,38 @@ function registerSearchDecisionsTool(server2, loader, getScope, onToolInvoked) {
82607
82634
  ]
82608
82635
  };
82609
82636
  } catch (e) {
82637
+ onToolError?.(e, { toolName: "search_decisions" });
82610
82638
  return loaderErrorResponse(e, "Failed to search decisions. Please try again.");
82611
82639
  }
82612
82640
  }
82613
82641
  );
82614
82642
  }
82615
- function registerApplyDecisionTool(server2, loader, getScope, onToolInvoked) {
82643
+ function registerApplyDecisionTool({ server: server2, loader, getScope, onToolInvoked, onToolError }) {
82616
82644
  server2.registerTool(
82617
82645
  "apply_decision",
82618
82646
  {
82619
82647
  title: "Apply Decision",
82620
- description: "Act on a resolved decision. Returns implementation kit with screenshot, element targeting, and step-by-step instructions. Set target to control output format: code changes, slideshow content, graphic specs, or prototype data. Set include to control detail level.",
82648
+ description: 'Retrieve the full implementation context for a UI change. Returns a multimodal response with screenshots, element metadata, and step-by-step instructions. Use include to select what is returned. For a full page screenshot with implementation instructions, use the default include="instructions". Do not use include="crop" expecting a full page \u2014 it returns only a cropped element.',
82621
82649
  inputSchema: {
82622
82650
  id: external_exports.string().describe("Decision ID from search_decisions"),
82623
- include: external_exports.enum(["implementation", "image", "context", "all"]).default("implementation").optional().describe(
82624
- "What to return: implementation (screenshot + instructions), image (cropped element), context (thread discussion), all (everything)"
82625
- ),
82626
- target: external_exports.enum(["code", "slideshow", "graphic", "prototype"]).default("code").optional().describe("Output format for instructions")
82651
+ include: external_exports.enum(["instructions", "crop", "discussion", "all"]).default("instructions").optional().describe(
82652
+ '"instructions" (default): full page screenshot + element metadata + instructions. "crop": cropped screenshot of the specific element only (falls back to full page when no element region exists). "discussion": thread discussion text, no images. "all": full page screenshot, cropped element, instructions, and thread context combined.'
82653
+ )
82627
82654
  },
82628
82655
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
82629
82656
  },
82630
- async ({ id, include = "implementation", target = "code" }) => {
82657
+ async ({ id, include = "instructions" }) => {
82631
82658
  onToolInvoked?.("apply_decision");
82632
82659
  try {
82633
- void target;
82634
82660
  return await handleApplyDecision(loader, id, include, loaderScope(getScope()));
82635
82661
  } catch (e) {
82662
+ onToolError?.(e, { toolName: "apply_decision" });
82636
82663
  return loaderErrorResponse(e, "Failed to apply decision. Please try again.");
82637
82664
  }
82638
82665
  }
82639
82666
  );
82640
82667
  }
82641
- function registerListCanvasesTool(server2, loader, getScope, onToolInvoked) {
82668
+ function registerListCanvasesTool({ server: server2, loader, getScope, onToolInvoked, onToolError }) {
82642
82669
  server2.registerTool(
82643
82670
  "list_canvases",
82644
82671
  {
@@ -82656,14 +82683,21 @@ function registerListCanvasesTool(server2, loader, getScope, onToolInvoked) {
82656
82683
  onToolInvoked?.("list_canvases");
82657
82684
  try {
82658
82685
  const result = await loader.listCanvases(params, loaderScope(getScope()));
82659
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
82686
+ const text = result.nextCursor ? `Showing ${result.canvases.length} canvases. Pass cursor "${result.nextCursor}" to see more.` : `Showing ${result.canvases.length} canvases.`;
82687
+ return {
82688
+ content: [
82689
+ { type: "text", text },
82690
+ { type: "text", text: JSON.stringify(result) }
82691
+ ]
82692
+ };
82660
82693
  } catch (e) {
82694
+ onToolError?.(e, { toolName: "list_canvases" });
82661
82695
  return loaderErrorResponse(e, "Failed to list canvases. Please try again.");
82662
82696
  }
82663
82697
  }
82664
82698
  );
82665
82699
  }
82666
- function registerListProjectsTool(server2, loader, getScope, onToolInvoked) {
82700
+ function registerListProjectsTool({ server: server2, loader, getScope, onToolInvoked, onToolError }) {
82667
82701
  server2.registerTool(
82668
82702
  "list_projects",
82669
82703
  {
@@ -82679,29 +82713,79 @@ function registerListProjectsTool(server2, loader, getScope, onToolInvoked) {
82679
82713
  onToolInvoked?.("list_projects");
82680
82714
  try {
82681
82715
  const result = await loader.listProjects(params, loaderScope(getScope()));
82682
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
82716
+ const text = result.nextCursor ? `Showing ${result.projects.length} projects. Pass cursor "${result.nextCursor}" to see more.` : `Showing ${result.projects.length} projects.`;
82717
+ return {
82718
+ content: [
82719
+ { type: "text", text },
82720
+ { type: "text", text: JSON.stringify(result) }
82721
+ ]
82722
+ };
82683
82723
  } catch (e) {
82724
+ onToolError?.(e, { toolName: "list_projects" });
82684
82725
  return loaderErrorResponse(e, "Failed to list projects. Please try again.");
82685
82726
  }
82686
82727
  }
82687
82728
  );
82688
82729
  }
82689
- function registerMcpTools(server2, loader, getScope, onToolInvoked) {
82690
- registerSearchDecisionsTool(server2, loader, getScope, onToolInvoked);
82691
- registerApplyDecisionTool(server2, loader, getScope, onToolInvoked);
82692
- registerListCanvasesTool(server2, loader, getScope, onToolInvoked);
82693
- registerListProjectsTool(server2, loader, getScope, onToolInvoked);
82730
+ function registerMcpTools(opts) {
82731
+ registerSearchDecisionsTool(opts);
82732
+ registerApplyDecisionTool(opts);
82733
+ registerListCanvasesTool(opts);
82734
+ registerListProjectsTool(opts);
82694
82735
  }
82736
+ var searchDecisionsSchema;
82695
82737
  var init_mcpToolRegistration = __esm({
82696
82738
  "../../libs/shared/src/mcpToolRegistration.ts"() {
82697
82739
  "use strict";
82698
82740
  init_zod2();
82699
82741
  init_decisionCanvas();
82700
82742
  init_mcpDecisionLoader();
82743
+ searchDecisionsSchema = {
82744
+ query: external_exports.string().optional().describe("Text search across headline, body, and element text"),
82745
+ status: external_exports.enum(["resolved", "suggested", "all"]).default("resolved").optional().describe("Filter by annotation status (default: resolved)"),
82746
+ limit: external_exports.number().int().min(1).max(100).default(20).optional().describe("Results per page (1-100, default 20)"),
82747
+ cursor: external_exports.string().optional().describe("Opaque pagination cursor from a previous response"),
82748
+ sort: external_exports.enum(["relevance", "resolved_at_desc", "created_at_desc"]).default("resolved_at_desc").optional().describe("Sort order"),
82749
+ canvasId: external_exports.string().uuid().optional().describe("Exact canvas ID"),
82750
+ canvasTitle: external_exports.string().optional().describe("Canvas title substring match"),
82751
+ canvasStatus: external_exports.enum(["active", "archived"]).optional().describe("Canvas lifecycle status"),
82752
+ pageId: external_exports.string().uuid().optional().describe("Exact page ID"),
82753
+ pageName: external_exports.string().optional().describe("Page name substring match"),
82754
+ projectId: external_exports.string().uuid().optional().describe("Exact project ID"),
82755
+ projectName: external_exports.string().optional().describe("Project name substring match"),
82756
+ threadId: external_exports.string().uuid().optional().describe("Filter by thread ID"),
82757
+ appName: external_exports.string().optional().describe("App name substring match"),
82758
+ windowTitle: external_exports.string().optional().describe("Window title substring match"),
82759
+ url: external_exports.string().optional().describe("URL substring match"),
82760
+ anchorState: external_exports.enum(["anchored", "reanchored", "detached"]).optional().describe(
82761
+ "Filter by anchor state (anchored = confirmed position, reanchored = moved, detached = element not found)"
82762
+ ),
82763
+ elementEid: external_exports.string().optional().describe("Exact element accessibility ID"),
82764
+ elementRole: external_exports.string().optional().describe("Exact element ARIA role"),
82765
+ elementInteractivity: external_exports.boolean().optional().describe("Filter to interactive elements only"),
82766
+ minEndorsements: external_exports.number().int().min(0).optional().describe("Minimum endorsement count"),
82767
+ hasReplies: external_exports.boolean().optional().describe("Filter to decisions with replies"),
82768
+ createdBy: external_exports.string().uuid().optional().describe("Creator user ID"),
82769
+ resolvedFrom: external_exports.string().optional().describe("ISO date lower bound for resolved_at \u2014 prefer resolved dates for finding recently approved decisions"),
82770
+ resolvedTo: external_exports.string().optional().describe("ISO date upper bound for resolved_at \u2014 prefer resolved dates for finding recently approved decisions"),
82771
+ createdFrom: external_exports.string().optional().describe("ISO date lower bound for created_at \u2014 prefer created dates for finding recently suggested decisions"),
82772
+ createdTo: external_exports.string().optional().describe("ISO date upper bound for created_at \u2014 prefer created dates for finding recently suggested decisions")
82773
+ };
82701
82774
  }
82702
82775
  });
82703
82776
 
82704
82777
  // src/decisionLoaderHttp.ts
82778
+ async function fetchWithRetry(url2, init3, retries = 1) {
82779
+ for (let attempt = 0; attempt <= retries; attempt++) {
82780
+ try {
82781
+ const response = await fetch(url2, init3);
82782
+ if (response.ok || attempt === retries) return response;
82783
+ } catch (err) {
82784
+ if (attempt === retries) throw err;
82785
+ }
82786
+ }
82787
+ throw new Error("Unreachable");
82788
+ }
82705
82789
  function readMcpCredentials() {
82706
82790
  const apiBase = (process.env.LACE_API_BASE ?? process.env.API_BASE_URL ?? "").trim().replace(/\/$/, "");
82707
82791
  const token = (process.env.LACE_DESKTOP_API_TOKEN ?? "").trim();
@@ -82776,7 +82860,7 @@ function cropUrl(config4, artifactId, bbox) {
82776
82860
  }
82777
82861
  async function fetchCrop(config4, artifactId, bbox) {
82778
82862
  try {
82779
- const res = await fetch(cropUrl(config4, artifactId, bbox), {
82863
+ const res = await fetchWithRetry(cropUrl(config4, artifactId, bbox), {
82780
82864
  headers: { Authorization: `Bearer ${config4.token}`, "x-org-id": config4.orgId },
82781
82865
  signal: AbortSignal.timeout(API_FETCH_TIMEOUT_MS)
82782
82866
  });
@@ -82806,7 +82890,7 @@ async function fetchDecisionThreadContext(decision) {
82806
82890
  }
82807
82891
  async function fetchArtifactPreviewUrl(artifactId, config4) {
82808
82892
  try {
82809
- const res = await fetch(
82893
+ const res = await fetchWithRetry(
82810
82894
  `${config4.apiBase}/lace/screenshots/artifacts/${encodeURIComponent(artifactId)}/preview-url?kind=full`,
82811
82895
  {
82812
82896
  headers: { Accept: "application/json", Authorization: `Bearer ${config4.token}`, "x-org-id": config4.orgId },
@@ -82892,7 +82976,7 @@ function createHttpDecisionLoader() {
82892
82976
  const context2 = await fetchDecisionThreadContext(decision);
82893
82977
  return context2 ? [{ text: context2 }] : [];
82894
82978
  },
82895
- async loadElementVocabulary(canvasId, _scope) {
82979
+ async loadElementVocabulary(canvasId, _scope, _pageId) {
82896
82980
  return fetchCanvasElementAnnotations(canvasId, requireConfig());
82897
82981
  },
82898
82982
  async listCanvases(query, _scope) {
@@ -82920,7 +83004,7 @@ var init_decisionLoaderHttp = __esm({
82920
83004
  init_decisionCanvas();
82921
83005
  init_mcpDecisionLoader();
82922
83006
  init_auth();
82923
- API_FETCH_TIMEOUT_MS = 1e4;
83007
+ API_FETCH_TIMEOUT_MS = 3e4;
82924
83008
  }
82925
83009
  });
82926
83010
 
@@ -82928,9 +83012,18 @@ var init_decisionLoaderHttp = __esm({
82928
83012
  function getLocalScope() {
82929
83013
  return { orgId: readMcpCredentials()?.orgId ?? "", userId: "local-mcp" };
82930
83014
  }
83015
+ function handleToolError(error51, context2) {
83016
+ esm_exports5.captureException(error51, { tags: { mcpTool: context2?.toolName ?? "unknown" } });
83017
+ }
82931
83018
  function createLaceServer() {
82932
83019
  const server2 = new McpServer({ name: "lace", version: "2.0.0" }, { instructions: LACE_MCP_INSTRUCTIONS });
82933
- registerMcpTools(server2, createHttpDecisionLoader(), getLocalScope, trackToolInvoked);
83020
+ registerMcpTools({
83021
+ server: server2,
83022
+ loader: createHttpDecisionLoader(),
83023
+ getScope: getLocalScope,
83024
+ onToolInvoked: trackToolInvoked,
83025
+ onToolError: handleToolError
83026
+ });
82934
83027
  return server2;
82935
83028
  }
82936
83029
  var init_server3 = __esm({
@@ -82941,6 +83034,7 @@ var init_server3 = __esm({
82941
83034
  init_mcpToolRegistration();
82942
83035
  init_analytics();
82943
83036
  init_decisionLoaderHttp();
83037
+ init_sentry();
82944
83038
  }
82945
83039
  });
82946
83040
 
@@ -82973,6 +83067,13 @@ var init_index = __esm({
82973
83067
  await flushTelemetry();
82974
83068
  throw err;
82975
83069
  }
83070
+ process.on("uncaughtException", (err) => {
83071
+ esm_exports5.captureException(err);
83072
+ void flushTelemetry().finally(() => process.exit(1));
83073
+ });
83074
+ process.on("unhandledRejection", (reason) => {
83075
+ esm_exports5.captureException(reason);
83076
+ });
82976
83077
  process.on("beforeExit", () => void flushTelemetry());
82977
83078
  process.on("SIGINT", () => void flushTelemetry().finally(() => process.exit(0)));
82978
83079
  process.on("SIGTERM", () => void flushTelemetry().finally(() => process.exit(0)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lace-mcp",
3
- "version": "0.0.2-alpha.19",
3
+ "version": "0.0.2-alpha.20",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist/cli.js",