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

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/README.md CHANGED
@@ -5,7 +5,7 @@ MCP server for [Lace](https://inlace.co). Connects your coding agent to approved
5
5
  ## How it works
6
6
 
7
7
  1. You review your product in Lace and approve a decision on the [Decision Canvas](https://docs.inlace.co/concepts/decision-canvas)
8
- 2. Your coding agent calls `list_decisions` to see what's been approved
8
+ 2. Your coding agent calls `search_decisions` to find what's been approved
9
9
  3. It calls `apply_decision` to get screenshots, element targeting, and step-by-step instructions
10
10
  4. The agent writes the code change and you review the diff
11
11
 
@@ -28,18 +28,19 @@ The Lace desktop app must be open and signed in for the MCP server to work.
28
28
 
29
29
  | Tool | Description |
30
30
  | --- | --- |
31
- | `list_decisions` | Returns all approved decisions as a summary list |
32
- | `apply_decision` | Everything needed to implement a decision: screenshots, targeting, and step-by-step instructions |
33
- | `get_decision_detail` | On-demand detail for a decision: cropped element image, thread context, or both |
31
+ | `search_decisions` | Search and filter resolved decisions with pagination. No params returns the top 20 |
32
+ | `apply_decision` | Implementation kit for a decision: screenshots, targeting, instructions. `include` controls detail level |
33
+ | `list_canvases` | Browse review canvases with page names and decision counts |
34
+ | `list_projects` | Browse projects with thread and canvas counts |
34
35
 
35
- Use `apply_decision` when implementing. Use `get_decision_detail` only when you need a cropped element image or thread context.
36
+ Use `search_decisions` to discover, then `apply_decision` to act. Use `list_canvases` and `list_projects` to find IDs for filtering.
36
37
 
37
38
  ## Usage
38
39
 
39
40
  Once installed, ask your coding agent:
40
41
 
41
42
  ```
42
- "List Lace decisions"
43
+ "Search Lace decisions"
43
44
  ```
44
45
 
45
46
  Or to implement directly:
package/SKILL.md CHANGED
@@ -9,15 +9,15 @@ Lace surfaces adoption insights as actionable decisions. When a decision is appr
9
9
 
10
10
  ## Steps
11
11
 
12
- 1. Call the `list_decisions` MCP tool to see all approved decisions.
13
- 2. If there is exactly one decision, proceed with it. If there are multiple, ask the user which one to implement.
14
- 3. Call the `apply_decision` MCP tool with the chosen decision ID.
15
- 4. Follow the implementation instructions returned by `apply_decision` exactly they include screenshots, element targeting, and constraints.
16
- 5. Implement the minimal code change described. Do not refactor or modify anything outside the target element.
17
- 6. If you need a cropped image of a specific UI element (e.g., for embedding in a prototype), call `get_decision_detail` with the decision ID and `include: "image"`.
18
- 7. If you need to understand why a decision was made or what alternatives were considered, call `get_decision_detail` with the decision ID and `include: "context"`.
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
+ 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
+ 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.
17
+ 6. Follow the implementation instructions returned by `apply_decision` exactly they include screenshots, element targeting, and constraints.
18
+ 7. Implement the minimal code change described. Do not refactor or modify anything outside the target element.
19
19
 
20
20
  ## Troubleshooting
21
21
 
22
- - If `list_decisions` returns an authentication error, tell the user to run `npx lace-mcp install <editor>` to re-authenticate.
23
- - If no decisions are returned, the user may need to approve a decision in the Lace overlay first.
22
+ - If `search_decisions` returns an authentication error, tell the user to run `npx lace-mcp install <editor>` to re-authenticate.
23
+ - If no decisions are returned, the user may need to approve a decision in the Lace overlay first or provide a narrower canvas/project scope.
package/dist/cli.js CHANGED
@@ -82165,10 +82165,13 @@ var init_zod2 = __esm({
82165
82165
  });
82166
82166
 
82167
82167
  // ../../libs/shared/src/decisionCanvas/types.ts
82168
+ function isFullPageBbox(target) {
82169
+ return target.bbox_x1 <= 0 && target.bbox_y1 <= 0 && target.bbox_x2 >= 1 && target.bbox_y2 >= 1;
82170
+ }
82168
82171
  function isCanvasDecisionAnnotation(annotation) {
82169
82172
  return annotation.kind == null || annotation.kind === CANVAS_ANNOTATION_KIND_DECISION;
82170
82173
  }
82171
- 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;
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;
82172
82175
  var init_types4 = __esm({
82173
82176
  "../../libs/shared/src/decisionCanvas/types.ts"() {
82174
82177
  "use strict";
@@ -82241,6 +82244,19 @@ var init_types4 = __esm({
82241
82244
  "canvas-updated",
82242
82245
  ...CANVAS_THREAD_CHANGE_EVENT_TYPES
82243
82246
  ];
82247
+ LACE_MCP_INSTRUCTIONS = `Lace surfaces UI decisions from product reviews. Decisions drive code changes, slideshows, graphics, and prototypes.
82248
+
82249
+ Discovery:
82250
+ - search_decisions: find decisions by text, canvas, project, app, time, or element. No params = top 20 ranked.
82251
+ - list_canvases: browse review canvases to find canvas IDs for filtering.
82252
+ - list_projects: browse projects to find project IDs for scoping.
82253
+
82254
+ 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
+
82257
+ Workflow: search_decisions -> pick a decision -> apply_decision with the right target.
82258
+
82259
+ 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.`;
82244
82260
  }
82245
82261
  });
82246
82262
 
@@ -82271,9 +82287,6 @@ function currentCommentPlacements(placements) {
82271
82287
  function resolvedDecisionPlacements(placements) {
82272
82288
  return currentCommentPlacements(placements).filter(isResolvedCommentPlacement);
82273
82289
  }
82274
- function resolvedDecisionViews(placements) {
82275
- return resolvedDecisionPlacements(placements);
82276
- }
82277
82290
  var init_readModel = __esm({
82278
82291
  "../../libs/shared/src/decisionCanvas/readModel.ts"() {
82279
82292
  "use strict";
@@ -82296,60 +82309,6 @@ function optionalAttr(name, value) {
82296
82309
  function deriveDecisionStatus(decision) {
82297
82310
  return decision.status;
82298
82311
  }
82299
- function canvasAnnotationToMcpDecisionView(decision) {
82300
- return {
82301
- id: decision.id,
82302
- commentLineageId: decision.comment_lineage_id,
82303
- source: "canvas",
82304
- headline: decision.headline,
82305
- body: decision.body,
82306
- elementEid: decision.element_eid,
82307
- shotId: decision.page.shot_id,
82308
- bbox: [decision.bbox_x1, decision.bbox_y1, decision.bbox_x2, decision.bbox_y2],
82309
- status: deriveDecisionStatus(decision),
82310
- anchorState: decision.anchor_state,
82311
- appName: decision.page.app_name,
82312
- windowTitle: decision.page.window_title,
82313
- createdAt: decision.created_at,
82314
- resolvedAt: decision.resolved_at
82315
- };
82316
- }
82317
- function dedupeKey(view) {
82318
- if (view.commentLineageId) return `lineage:${view.commentLineageId}`;
82319
- if (view.elementEid && view.shotId) return `${view.elementEid}:${view.shotId}`;
82320
- return `canvas:${view.id}`;
82321
- }
82322
- function decisionPlacementTime(view) {
82323
- const createdAt = Date.parse(view.createdAt);
82324
- if (Number.isFinite(createdAt)) return createdAt;
82325
- const resolvedAt = view.resolvedAt ? Date.parse(view.resolvedAt) : Number.NaN;
82326
- if (Number.isFinite(resolvedAt)) return resolvedAt;
82327
- return 0;
82328
- }
82329
- function scoreDecision(view, maxAgeMs, now) {
82330
- const resolvedAt = view.resolvedAt ? Date.parse(view.resolvedAt) : Number.NaN;
82331
- const ageMs = Number.isFinite(resolvedAt) ? Math.max(0, now.getTime() - resolvedAt) : maxAgeMs;
82332
- const recency = maxAgeMs <= 0 ? 1 : 1 - Math.min(ageMs, maxAgeMs) / maxAgeMs;
82333
- const elementBoost = view.elementEid != null ? 0.3 : 0;
82334
- const bodyBoost = view.body != null && view.body.length > 20 ? 1 : 0;
82335
- return 0.5 * recency + 0.3 * elementBoost + 0.2 * bodyBoost;
82336
- }
82337
- function rankMcpDecisionViews(views, now = /* @__PURE__ */ new Date()) {
82338
- const ages = views.map((view) => view.resolvedAt ? now.getTime() - Date.parse(view.resolvedAt) : 0).filter((age) => Number.isFinite(age) && age >= 0);
82339
- const maxAgeMs = Math.max(0, ...ages);
82340
- return views.slice().sort((a, b) => scoreDecision(b, maxAgeMs, now) - scoreDecision(a, maxAgeMs, now));
82341
- }
82342
- function dedupeMcpDecisionEntries(entries) {
82343
- const seen = /* @__PURE__ */ new Map();
82344
- for (const entry of entries) {
82345
- const key = dedupeKey(entry.view);
82346
- const existing = seen.get(key);
82347
- if (!existing || decisionPlacementTime(entry.view) > decisionPlacementTime(existing.view)) {
82348
- seen.set(key, entry);
82349
- }
82350
- }
82351
- return Array.from(seen.values());
82352
- }
82353
82312
  function deriveFlowContext(decision, allDecisions) {
82354
82313
  const siblings = resolvedDecisionPlacements(allDecisions).filter(
82355
82314
  (candidate) => candidate.canvas_id === decision.canvas_id
@@ -82385,8 +82344,8 @@ function canvasAnnotationToDecisionXmlInput(decision, allDecisions) {
82385
82344
  elementEid: decision.element_eid,
82386
82345
  createdAt: decision.created_at,
82387
82346
  resolvedAt: decision.resolved_at,
82388
- resolvedBy: null,
82389
- bbox: decision.anchor_state === "detached" ? null : [decision.bbox_x1, decision.bbox_y1, decision.bbox_x2, decision.bbox_y2],
82347
+ 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],
82390
82349
  element: {
82391
82350
  role: decision.element_role,
82392
82351
  label: decision.element_visible_text,
@@ -82430,10 +82389,21 @@ function serializeDecisionXml(decision) {
82430
82389
  "</decision>"
82431
82390
  ].filter(Boolean).join("\n");
82432
82391
  }
82433
- function formatApplyDecisionText(decision, allDecisions, vocabulary) {
82392
+ function formatApplyDecisionText(decision, allDecisions, vocabulary, threadContext) {
82434
82393
  const xml = serializeDecisionXml(canvasAnnotationToDecisionXmlInput(decision, allDecisions));
82435
82394
  const parts = [xml];
82436
82395
  if (vocabulary) parts.push(vocabulary);
82396
+ if (threadContext && threadContext.length > 0) {
82397
+ const messages = threadContext.map((m) => {
82398
+ if ("role" in m) {
82399
+ return ` <message role="${m.role}" at="${m.createdAt}">${esc3(m.visibleText ?? "")}</message>`;
82400
+ }
82401
+ return ` <message>${esc3(m.text)}</message>`;
82402
+ });
82403
+ parts.push(`<thread>
82404
+ ${messages.join("\n")}
82405
+ </thread>`);
82406
+ }
82437
82407
  parts.push(
82438
82408
  "Apply the resolved Decision Canvas change. Use the element context, bbox, and screen metadata above as implementation grounding."
82439
82409
  );
@@ -82453,6 +82423,7 @@ var init_mcpFormat = __esm({
82453
82423
  "../../libs/shared/src/decisionCanvas/mcpFormat.ts"() {
82454
82424
  "use strict";
82455
82425
  init_readModel();
82426
+ init_types4();
82456
82427
  MAX_CANVAS_VOCABULARY_ELEMENTS = 100;
82457
82428
  MCP_FETCH_TIMEOUT_MS = 1e4;
82458
82429
  }
@@ -82495,12 +82466,6 @@ var init_mcpDecisionLoader = __esm({
82495
82466
  });
82496
82467
 
82497
82468
  // ../../libs/shared/src/mcpToolRegistration.ts
82498
- function notFoundError(id) {
82499
- return {
82500
- content: [{ type: "text", text: `Decision '${id}' not found. Use list_decisions to see available decisions.` }],
82501
- isError: true
82502
- };
82503
- }
82504
82469
  function loaderErrorResponse(error51, fallback) {
82505
82470
  if (error51 instanceof DecisionLoaderError) {
82506
82471
  return { content: [{ type: "text", text: error51.contentText }], isError: true };
@@ -82508,10 +82473,11 @@ function loaderErrorResponse(error51, fallback) {
82508
82473
  return { content: [{ type: "text", text: fallback }], isError: true };
82509
82474
  }
82510
82475
  function loaderScope(scope) {
82511
- return { orgId: scope.orgId };
82476
+ return { orgId: scope.orgId, userId: scope.userId };
82512
82477
  }
82513
82478
  function isValidBbox(decision) {
82514
- return [decision.bbox_x1, decision.bbox_y1, decision.bbox_x2, decision.bbox_y2].every(Number.isFinite) && decision.bbox_x1 < decision.bbox_x2 && decision.bbox_y1 < decision.bbox_y2;
82479
+ return [decision.bbox_x1, decision.bbox_y1, decision.bbox_x2, decision.bbox_y2].every(Number.isFinite) && decision.bbox_x1 < decision.bbox_x2 && decision.bbox_y1 < decision.bbox_y2 && // Page-scoped comments are stored as a full-page box; they have no region to crop.
82480
+ !isFullPageBbox(decision);
82515
82481
  }
82516
82482
  function placementTimestamp(decision) {
82517
82483
  const createdAt = Date.parse(decision.created_at);
@@ -82538,122 +82504,194 @@ function decisionBbox(decision) {
82538
82504
  if (!isValidBbox(decision)) return null;
82539
82505
  return [decision.bbox_x1, decision.bbox_y1, decision.bbox_x2, decision.bbox_y2];
82540
82506
  }
82541
- function formatThreadContext(messages) {
82542
- const lines = messages.map((message) => {
82543
- if ("text" in message) return message.text;
82544
- if (!message.visibleText) return null;
82545
- return `**${message.role === "user" ? "User" : "Assistant"}** (${message.createdAt}):
82546
- ${message.visibleText}`;
82547
- }).filter((line) => Boolean(line));
82548
- return lines.length > 0 ? lines.join("\n\n") : null;
82507
+ function decisionNotFoundResult(id) {
82508
+ return {
82509
+ content: [{ type: "text", text: `Decision "${id}" not found. Use search_decisions to find valid IDs.` }],
82510
+ isError: true
82511
+ };
82549
82512
  }
82550
- async function loadDetail(loader, id, scope) {
82551
- const detail = await loader.loadDecisionDetail(id, scope);
82552
- if (!detail) {
82553
- throw new DecisionLoaderError(`Decision '${id}' not found. Use list_decisions to see available decisions.`);
82554
- }
82513
+ async function appendImplementationContent(context2, include) {
82514
+ const detail = await context2.loader.loadDecisionDetail(context2.id, context2.scope);
82515
+ if (!detail) return null;
82516
+ context2.content.push(...detail.images);
82517
+ const vocabulary = await context2.loader.loadElementVocabulary(detail.decision.canvas_id, context2.scope);
82518
+ const vocabStr = vocabulary.length > 0 ? buildCanvasVocabulary(vocabulary) : null;
82519
+ const threadContext = include === "all" ? await context2.loader.loadThreadContext(context2.id, context2.scope) : void 0;
82520
+ context2.content.push({
82521
+ type: "text",
82522
+ text: formatApplyDecisionText(detail.decision, detail.allDecisions, vocabStr, threadContext)
82523
+ });
82555
82524
  return detail;
82556
82525
  }
82557
- async function loadElementImageContent(loader, detail, scope) {
82526
+ async function appendImageContent(context2, existingDetail) {
82527
+ const detail = existingDetail ?? await context2.loader.loadDecisionDetail(context2.id, context2.scope);
82528
+ if (!detail) return null;
82558
82529
  const cropSource = resolveCropSource(detail);
82559
- if (!cropSource) {
82560
- return {
82561
- type: "text",
82562
- text: "No element image available - decision has no valid bounding box or screenshot."
82563
- };
82564
- }
82565
- const bbox = decisionBbox(cropSource);
82566
- const image = bbox ? await loader.cropElement(cropSource.page.artifact_id, bbox, scope) : null;
82567
- return image ? { type: "image", data: image, mimeType: "image/png" } : {
82568
- type: "text",
82569
- text: "No element image available - decision has no valid bounding box or screenshot."
82570
- };
82530
+ const bbox = cropSource ? decisionBbox(cropSource) : null;
82531
+ if (!cropSource || !bbox) return detail;
82532
+ const cropped = await context2.loader.cropElement(cropSource.page.artifact_id, bbox, context2.scope);
82533
+ if (cropped) context2.content.push({ type: "image", data: cropped, mimeType: "image/png" });
82534
+ return detail;
82571
82535
  }
82572
- async function loadThreadContextContent(loader, id, scope) {
82573
- const threadContext = formatThreadContext(await loader.loadThreadContext(id, scope));
82574
- return { type: "text", text: threadContext ?? "No thread discussion linked to this decision." };
82536
+ async function appendContextContent(content, loader, id, scope) {
82537
+ const messages = await loader.loadThreadContext(id, scope);
82538
+ if (messages.length === 0) {
82539
+ content.push({ type: "text", text: "No thread context available for this decision." });
82540
+ return;
82541
+ }
82542
+ const formatted = messages.map((message) => "role" in message ? `[${message.role}] ${message.visibleText ?? ""}` : message.text).join("\n");
82543
+ content.push({ type: "text", text: formatted });
82575
82544
  }
82576
- async function loadDecisionDetailContent(loader, id, include, scope) {
82577
- const detail = await loadDetail(loader, id, scope);
82545
+ async function handleApplyDecision(loader, id, include, scope) {
82578
82546
  const content = [];
82547
+ const context2 = { content, loader, id, scope };
82548
+ let detail = null;
82549
+ if (include === "implementation" || include === "all") {
82550
+ detail = await appendImplementationContent(context2, include);
82551
+ if (!detail) return decisionNotFoundResult(id);
82552
+ }
82579
82553
  if (include === "image" || include === "all") {
82580
- content.push(await loadElementImageContent(loader, detail, scope));
82554
+ detail = await appendImageContent(context2, detail);
82581
82555
  }
82582
- if (include === "context" || include === "all") {
82583
- content.push(await loadThreadContextContent(loader, id, scope));
82556
+ if (include === "context" && content.length === 0) {
82557
+ await appendContextContent(content, loader, id, scope);
82584
82558
  }
82585
- return content;
82559
+ return content.length > 0 ? { content } : decisionNotFoundResult(id);
82586
82560
  }
82587
- function registerMcpTools(server2, loader, getScope, onToolInvoked) {
82561
+ function registerSearchDecisionsTool(server2, loader, getScope, onToolInvoked) {
82588
82562
  server2.registerTool(
82589
- "list_decisions",
82563
+ "search_decisions",
82590
82564
  {
82591
- title: "List Decisions",
82592
- description: "List all resolved product decisions. Returns summary fields: id, headline, elementEid, appName, resolvedAt. Use apply_decision to implement a specific decision, or get_decision_detail for element images and thread context.",
82593
- annotations: { readOnlyHint: true, destructiveHint: false }
82565
+ 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.",
82567
+ 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
+ }
82594
82597
  },
82595
- async () => {
82596
- onToolInvoked?.("list_decisions");
82598
+ async (params) => {
82599
+ onToolInvoked?.("search_decisions");
82597
82600
  try {
82598
- const scope = loaderScope(getScope());
82599
- const decisions = await loader.listDecisions(scope);
82600
- if (decisions.length === 0) {
82601
- return { content: [{ type: "text", text: "No resolved decisions yet." }] };
82602
- }
82603
- return { content: [{ type: "text", text: JSON.stringify(decisions, null, 2) }] };
82604
- } catch (error51) {
82605
- return loaderErrorResponse(error51, "Failed to fetch decisions. Please try again.");
82601
+ const result = await loader.searchDecisions(params, loaderScope(getScope()));
82602
+ const text = result.nextCursor ? `Showing ${result.decisions.length} of ${result.totalCount} results. Pass cursor "${result.nextCursor}" to see more.` : `Showing ${result.decisions.length} of ${result.totalCount} results.`;
82603
+ return {
82604
+ content: [
82605
+ { type: "text", text },
82606
+ { type: "text", text: JSON.stringify(result) }
82607
+ ]
82608
+ };
82609
+ } catch (e) {
82610
+ return loaderErrorResponse(e, "Failed to search decisions. Please try again.");
82606
82611
  }
82607
82612
  }
82608
82613
  );
82614
+ }
82615
+ function registerApplyDecisionTool(server2, loader, getScope, onToolInvoked) {
82609
82616
  server2.registerTool(
82610
82617
  "apply_decision",
82611
82618
  {
82612
82619
  title: "Apply Decision",
82613
- description: "Get a resolved decision with a screenshot and implementation instructions. Returns everything needed to implement the code change: decision context, element targeting, page screenshot, and step-by-step instructions. Use get_decision_detail only when you need a cropped element image or thread context.",
82614
- inputSchema: { id: external_exports.string().describe("Decision ID from list_decisions") },
82615
- annotations: { readOnlyHint: false, destructiveHint: false }
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.",
82621
+ inputSchema: {
82622
+ 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")
82627
+ },
82628
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
82616
82629
  },
82617
- async ({ id }) => {
82630
+ async ({ id, include = "implementation", target = "code" }) => {
82618
82631
  onToolInvoked?.("apply_decision");
82619
82632
  try {
82620
- const scope = loaderScope(getScope());
82621
- const detail = await loader.loadDecisionDetail(id, scope);
82622
- if (!detail) return notFoundError(id);
82623
- const vocabulary = buildCanvasVocabulary(await loader.loadElementVocabulary(detail.decision.canvas_id, scope));
82624
- return {
82625
- content: [
82626
- ...detail.images,
82627
- { type: "text", text: formatApplyDecisionText(detail.decision, detail.allDecisions, vocabulary) }
82628
- ]
82629
- };
82630
- } catch (error51) {
82631
- return loaderErrorResponse(error51, "Failed to apply decision. Please try again.");
82633
+ void target;
82634
+ return await handleApplyDecision(loader, id, include, loaderScope(getScope()));
82635
+ } catch (e) {
82636
+ return loaderErrorResponse(e, "Failed to apply decision. Please try again.");
82637
+ }
82638
+ }
82639
+ );
82640
+ }
82641
+ function registerListCanvasesTool(server2, loader, getScope, onToolInvoked) {
82642
+ server2.registerTool(
82643
+ "list_canvases",
82644
+ {
82645
+ title: "List Canvases",
82646
+ description: "List review canvases in the current org. Returns title, project, page names, and decision counts per page. Call this when the user mentions a specific design file, screen, or review session by name \u2014 use the returned canvas IDs and titles to scope search_decisions queries.",
82647
+ inputSchema: {
82648
+ status: external_exports.enum(["active", "archived"]).default("active").optional(),
82649
+ projectId: external_exports.string().uuid().optional().describe("Filter by project ID"),
82650
+ limit: external_exports.number().int().min(1).max(100).default(50).optional(),
82651
+ cursor: external_exports.string().optional()
82652
+ },
82653
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
82654
+ },
82655
+ async (params) => {
82656
+ onToolInvoked?.("list_canvases");
82657
+ try {
82658
+ const result = await loader.listCanvases(params, loaderScope(getScope()));
82659
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
82660
+ } catch (e) {
82661
+ return loaderErrorResponse(e, "Failed to list canvases. Please try again.");
82632
82662
  }
82633
82663
  }
82634
82664
  );
82665
+ }
82666
+ function registerListProjectsTool(server2, loader, getScope, onToolInvoked) {
82635
82667
  server2.registerTool(
82636
- "get_decision_detail",
82668
+ "list_projects",
82637
82669
  {
82638
- title: "Get Decision Detail",
82639
- description: "Get additional detail for a decision on demand. image = cropped PNG of the targeted UI element (for prototypes). context = thread discussion with rationale and alternatives. all = both. Use list_decisions to find decision IDs.",
82670
+ title: "List Projects",
82671
+ description: "List projects in the current org. Returns name, thread count, and canvas count. Call this when the user references a project by name \u2014 use the returned project IDs to pass as projectId or projectName filters to search_decisions.",
82640
82672
  inputSchema: {
82641
- id: external_exports.string().describe("Decision ID from list_decisions"),
82642
- include: external_exports.enum(["image", "context", "all"]).describe("What to include: image (cropped element), context (thread discussion), or all")
82673
+ limit: external_exports.number().int().min(1).max(100).default(50).optional(),
82674
+ cursor: external_exports.string().optional()
82643
82675
  },
82644
- annotations: { readOnlyHint: true, destructiveHint: false }
82676
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
82645
82677
  },
82646
- async ({ id, include }) => {
82647
- onToolInvoked?.("get_decision_detail");
82678
+ async (params) => {
82679
+ onToolInvoked?.("list_projects");
82648
82680
  try {
82649
- const scope = loaderScope(getScope());
82650
- return { content: await loadDecisionDetailContent(loader, id, include, scope) };
82651
- } catch (error51) {
82652
- return loaderErrorResponse(error51, "Failed to fetch decision detail.");
82681
+ const result = await loader.listProjects(params, loaderScope(getScope()));
82682
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
82683
+ } catch (e) {
82684
+ return loaderErrorResponse(e, "Failed to list projects. Please try again.");
82653
82685
  }
82654
82686
  }
82655
82687
  );
82656
82688
  }
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);
82694
+ }
82657
82695
  var init_mcpToolRegistration = __esm({
82658
82696
  "../../libs/shared/src/mcpToolRegistration.ts"() {
82659
82697
  "use strict";
@@ -82671,9 +82709,12 @@ function readMcpCredentials() {
82671
82709
  if (apiBase && token && orgId) return { apiBase, token, orgId };
82672
82710
  return loadCachedCredentials();
82673
82711
  }
82674
- async function fetchCanvasDecisions(config4) {
82712
+ async function fetchCanvasDecisions(config4, options = {}) {
82675
82713
  try {
82676
- const response = await fetch(`${config4.apiBase}/lace/canvases/decisions`, {
82714
+ const params = new URLSearchParams();
82715
+ if (options.canvasId) params.set("canvasId", options.canvasId);
82716
+ const query = params.toString();
82717
+ const response = await fetch(`${config4.apiBase}/lace/canvases/decisions${query ? `?${query}` : ""}`, {
82677
82718
  headers: {
82678
82719
  Accept: "application/json",
82679
82720
  Authorization: `Bearer ${config4.token}`,
@@ -82691,44 +82732,25 @@ async function fetchCanvasDecisions(config4) {
82691
82732
  return { ok: false, status: 0, error: "Network error or timeout" };
82692
82733
  }
82693
82734
  }
82694
- function loadErrorText(result) {
82695
- if (result.credentialsMissing) {
82696
- return "No API credentials configured. Run `npx lace-mcp install <claude|cursor|codex>` to authenticate.";
82697
- }
82698
- if (!result.error) return null;
82699
- const hint = result.error.status === 401 || result.error.status === 403 ? " Your token may be expired \u2014 run `npx lace-mcp install <editor>` to re-authenticate." : "";
82700
- return `Failed to fetch decisions (${result.error.status}: ${result.error.message}).${hint}`;
82701
- }
82702
- async function loadDecisionEntriesOrThrow() {
82703
- const result = await loadMcpDecisionEntries();
82704
- const errorText = loadErrorText(result);
82705
- if (errorText) throw new DecisionLoaderError(errorText);
82706
- return { entries: result.entries, allDecisions: result.allDecisions };
82735
+ async function fetchJson(config4, path2) {
82736
+ const response = await fetch(`${config4.apiBase}${path2}`, {
82737
+ headers: { Accept: "application/json", Authorization: `Bearer ${config4.token}`, "x-org-id": config4.orgId },
82738
+ signal: AbortSignal.timeout(API_FETCH_TIMEOUT_MS)
82739
+ });
82740
+ if (!response.ok) throw new DecisionLoaderError(`Request failed (${response.status}: ${response.statusText})`);
82741
+ return await response.json();
82707
82742
  }
82708
- async function loadMcpDecisionEntries() {
82709
- const config4 = readMcpCredentials();
82710
- if (!config4) {
82711
- return { entries: [], allDecisions: [], error: null, credentialsMissing: true };
82743
+ function searchParamsFromRecord(query) {
82744
+ const params = new URLSearchParams();
82745
+ for (const [key, value] of Object.entries(query)) {
82746
+ if (value != null) params.set(key, String(value));
82712
82747
  }
82713
- const result = await fetchCanvasDecisions(config4);
82714
- if (!result.ok) {
82715
- return {
82716
- entries: [],
82717
- allDecisions: [],
82718
- error: { status: result.status, message: result.error },
82719
- credentialsMissing: false
82720
- };
82721
- }
82722
- const decisions = resolvedDecisionViews(result.decisions);
82723
- const canvasEntries = decisions.map((decision) => ({
82724
- view: canvasAnnotationToMcpDecisionView(decision),
82725
- canvas: decision
82726
- }));
82727
- const allDecisions = canvasEntries.map((entry) => entry.canvas);
82728
- const deduped = dedupeMcpDecisionEntries(canvasEntries);
82729
- const ranked = rankMcpDecisionViews(deduped.map((entry) => entry.view)).slice(0, 8);
82730
- const entries = ranked.map((view) => deduped.find((entry) => entry.view.id === view.id)).filter((entry) => Boolean(entry));
82731
- return { entries, allDecisions, error: null, credentialsMissing: false };
82748
+ return params;
82749
+ }
82750
+ async function fetchDecisionSearch(config4, query) {
82751
+ const params = searchParamsFromRecord(query);
82752
+ const qs = params.toString();
82753
+ return fetchJson(config4, `/lace/canvases/decisions/search${qs ? `?${qs}` : ""}`);
82732
82754
  }
82733
82755
  async function fetchCanvasElementAnnotations(canvasId, config4) {
82734
82756
  try {
@@ -82835,38 +82857,59 @@ function requireConfig() {
82835
82857
  }
82836
82858
  return config4;
82837
82859
  }
82838
- function findDecision(id, entries, allDecisions) {
82839
- return entries.find((entry) => entry.view.id === id)?.canvas ?? allDecisions.find((decision) => decision.id === id) ?? null;
82860
+ async function loadDecisionDetailFromHttp(id, config4) {
82861
+ const searchResult = await fetchDecisionSearch(config4, { id, limit: 1 });
82862
+ const view = searchResult.decisions[0];
82863
+ if (!view) return null;
82864
+ const rawResult = await fetchCanvasDecisions(config4, { canvasId: view.canvasId });
82865
+ if (!rawResult.ok) {
82866
+ throw new DecisionLoaderError(`Decision detail failed (${rawResult.status}: ${rawResult.error})`);
82867
+ }
82868
+ const decision = rawResult.decisions.find((candidate) => candidate.id === id) ?? null;
82869
+ if (!decision) return null;
82870
+ return {
82871
+ decision,
82872
+ allDecisions: rawResult.decisions,
82873
+ images: await loadCanvasImages(decision)
82874
+ };
82840
82875
  }
82841
82876
  function createHttpDecisionLoader() {
82842
82877
  return {
82843
- async listDecisions() {
82844
- const result = await loadDecisionEntriesOrThrow();
82845
- return result.entries.map((entry) => entry.view);
82878
+ async searchDecisions(query, _scope) {
82879
+ return fetchDecisionSearch(requireConfig(), query);
82846
82880
  },
82847
82881
  async loadDecisionDetail(id) {
82848
- const result = await loadDecisionEntriesOrThrow();
82849
- const entry = result.entries.find((candidate) => candidate.view.id === id);
82850
- if (!entry) return null;
82851
- return {
82852
- decision: entry.canvas,
82853
- allDecisions: result.allDecisions,
82854
- images: await loadCanvasImages(entry.canvas)
82855
- };
82882
+ return loadDecisionDetailFromHttp(id, requireConfig());
82856
82883
  },
82857
82884
  async cropElement(artifactId, bbox) {
82858
82885
  const config4 = requireConfig();
82859
82886
  return await fetchCrop(config4, artifactId, bbox) ?? "";
82860
82887
  },
82861
82888
  async loadThreadContext(decisionId) {
82862
- const result = await loadDecisionEntriesOrThrow();
82863
- const decision = findDecision(decisionId, result.entries, result.allDecisions);
82889
+ const detail = await loadDecisionDetailFromHttp(decisionId, requireConfig());
82890
+ const decision = detail?.decision;
82864
82891
  if (!decision) return [];
82865
82892
  const context2 = await fetchDecisionThreadContext(decision);
82866
82893
  return context2 ? [{ text: context2 }] : [];
82867
82894
  },
82868
82895
  async loadElementVocabulary(canvasId, _scope) {
82869
82896
  return fetchCanvasElementAnnotations(canvasId, requireConfig());
82897
+ },
82898
+ async listCanvases(query, _scope) {
82899
+ const config4 = requireConfig();
82900
+ const params = new URLSearchParams();
82901
+ if (query.status) params.set("status", query.status);
82902
+ if (query.projectId) params.set("projectId", query.projectId);
82903
+ params.set("limit", String(query.limit ?? 50));
82904
+ if (query.cursor) params.set("cursor", query.cursor);
82905
+ return fetchJson(config4, `/lace/canvases?${params}`);
82906
+ },
82907
+ async listProjects(query, _scope) {
82908
+ const config4 = requireConfig();
82909
+ const params = new URLSearchParams();
82910
+ params.set("limit", String(query.limit ?? 50));
82911
+ if (query.cursor) params.set("cursor", query.cursor);
82912
+ return fetchJson(config4, `/projects?${params}`);
82870
82913
  }
82871
82914
  };
82872
82915
  }
@@ -82886,28 +82929,18 @@ function getLocalScope() {
82886
82929
  return { orgId: readMcpCredentials()?.orgId ?? "", userId: "local-mcp" };
82887
82930
  }
82888
82931
  function createLaceServer() {
82889
- const server2 = new McpServer({ name: "lace", version: "0.0.1" }, { instructions: LACE_INSTRUCTIONS });
82932
+ const server2 = new McpServer({ name: "lace", version: "2.0.0" }, { instructions: LACE_MCP_INSTRUCTIONS });
82890
82933
  registerMcpTools(server2, createHttpDecisionLoader(), getLocalScope, trackToolInvoked);
82891
82934
  return server2;
82892
82935
  }
82893
- var LACE_INSTRUCTIONS;
82894
82936
  var init_server3 = __esm({
82895
82937
  "src/server.ts"() {
82896
82938
  "use strict";
82897
82939
  init_mcp();
82940
+ init_types4();
82898
82941
  init_mcpToolRegistration();
82899
82942
  init_analytics();
82900
82943
  init_decisionLoaderHttp();
82901
- LACE_INSTRUCTIONS = `Lace is an adoption-diagnosis companion for desktop apps. It surfaces UI insights in an overlay and lets users approve decisions that should become code changes.
82902
-
82903
- When the user asks about Lace decisions, approved changes, or wants to implement an approved decision:
82904
-
82905
- 1. Call list_decisions to see all approved decisions.
82906
- 2. Call apply_decision with the decision ID \u2014 this returns a screenshot, the decision card, element targeting, and implementation instructions. Follow those instructions to implement the change.
82907
- 3. If you need a cropped image of the targeted element (e.g., for a prototype), call get_decision_detail with include="image".
82908
- 4. If you need to understand why a decision was made, call get_decision_detail with include="context".
82909
-
82910
- If the user says "implement the Lace decision", "apply the approved change", "what was approved", or references a Lace insight \u2014 start with list_decisions.`;
82911
82944
  }
82912
82945
  });
82913
82946
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lace-mcp",
3
- "version": "0.0.2-alpha.17",
3
+ "version": "0.0.2-alpha.19",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist/cli.js",