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 +7 -6
- package/SKILL.md +9 -9
- package/dist/cli.js +245 -212
- package/package.json +1 -1
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 `
|
|
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
|
-
| `
|
|
32
|
-
| `apply_decision` |
|
|
33
|
-
| `
|
|
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 `
|
|
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
|
-
"
|
|
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 `
|
|
13
|
-
2. If
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
16
|
-
5.
|
|
17
|
-
6.
|
|
18
|
-
7.
|
|
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 `
|
|
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
|
|
82542
|
-
|
|
82543
|
-
|
|
82544
|
-
|
|
82545
|
-
|
|
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
|
|
82551
|
-
const detail = await loader.loadDecisionDetail(id, scope);
|
|
82552
|
-
if (!detail)
|
|
82553
|
-
|
|
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
|
|
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
|
-
|
|
82560
|
-
|
|
82561
|
-
|
|
82562
|
-
|
|
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
|
|
82573
|
-
const
|
|
82574
|
-
|
|
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
|
|
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
|
-
|
|
82554
|
+
detail = await appendImageContent(context2, detail);
|
|
82581
82555
|
}
|
|
82582
|
-
if (include === "context"
|
|
82583
|
-
|
|
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
|
|
82561
|
+
function registerSearchDecisionsTool(server2, loader, getScope, onToolInvoked) {
|
|
82588
82562
|
server2.registerTool(
|
|
82589
|
-
"
|
|
82563
|
+
"search_decisions",
|
|
82590
82564
|
{
|
|
82591
|
-
title: "
|
|
82592
|
-
description: "
|
|
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?.("
|
|
82598
|
+
async (params) => {
|
|
82599
|
+
onToolInvoked?.("search_decisions");
|
|
82597
82600
|
try {
|
|
82598
|
-
const
|
|
82599
|
-
const
|
|
82600
|
-
|
|
82601
|
-
|
|
82602
|
-
|
|
82603
|
-
|
|
82604
|
-
|
|
82605
|
-
|
|
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: "
|
|
82614
|
-
inputSchema: {
|
|
82615
|
-
|
|
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
|
-
|
|
82621
|
-
|
|
82622
|
-
|
|
82623
|
-
|
|
82624
|
-
|
|
82625
|
-
|
|
82626
|
-
|
|
82627
|
-
|
|
82628
|
-
|
|
82629
|
-
|
|
82630
|
-
|
|
82631
|
-
|
|
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
|
-
"
|
|
82668
|
+
"list_projects",
|
|
82637
82669
|
{
|
|
82638
|
-
title: "
|
|
82639
|
-
description: "
|
|
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
|
-
|
|
82642
|
-
|
|
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 (
|
|
82647
|
-
onToolInvoked?.("
|
|
82678
|
+
async (params) => {
|
|
82679
|
+
onToolInvoked?.("list_projects");
|
|
82648
82680
|
try {
|
|
82649
|
-
const
|
|
82650
|
-
return { content:
|
|
82651
|
-
} catch (
|
|
82652
|
-
return loaderErrorResponse(
|
|
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
|
|
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
|
|
82695
|
-
|
|
82696
|
-
|
|
82697
|
-
|
|
82698
|
-
|
|
82699
|
-
|
|
82700
|
-
return
|
|
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
|
-
|
|
82709
|
-
const
|
|
82710
|
-
|
|
82711
|
-
|
|
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
|
-
|
|
82714
|
-
|
|
82715
|
-
|
|
82716
|
-
|
|
82717
|
-
|
|
82718
|
-
|
|
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
|
|
82839
|
-
|
|
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
|
|
82844
|
-
|
|
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
|
-
|
|
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
|
|
82863
|
-
const decision =
|
|
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
|
|
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
|
|