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.
- package/SKILL.md +2 -2
- package/dist/cli.js +183 -82
- 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.
|
|
16
|
-
5. Use `include: "
|
|
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 <=
|
|
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
|
|
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
|
|
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 `<
|
|
82336
|
-
${elements.map((element) => ` <
|
|
82337
|
-
</
|
|
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 : [
|
|
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
|
-
`<
|
|
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("
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
82517
|
-
|
|
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)
|
|
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(
|
|
82543
|
-
|
|
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 === "
|
|
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 === "
|
|
82588
|
+
if (include === "crop" || include === "all") {
|
|
82554
82589
|
detail = await appendImageContent(context2, detail);
|
|
82555
82590
|
}
|
|
82556
|
-
if (include === "
|
|
82591
|
+
if (include === "discussion" && content.length === 0) {
|
|
82557
82592
|
await appendContextContent(content, loader, id, scope);
|
|
82558
82593
|
}
|
|
82559
|
-
|
|
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(
|
|
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:
|
|
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(["
|
|
82624
|
-
"
|
|
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 = "
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
82690
|
-
registerSearchDecisionsTool(
|
|
82691
|
-
registerApplyDecisionTool(
|
|
82692
|
-
registerListCanvasesTool(
|
|
82693
|
-
registerListProjectsTool(
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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)));
|