@loupekit/mcp 0.8.1-next.29 → 0.9.0-next.30
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/dist/index.js +31 -13
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -39,6 +39,17 @@ async function fetchImage(url) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
var targetOf = (c) => c.kind === "free" ? "page-level note" : c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : c.anchor.cssPath;
|
|
42
|
+
var titleOf = (c) => c.title || (c.body.split("\n")[0] ?? "").trim() || "(no title)";
|
|
43
|
+
async function imageBlocks(attachments) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const a of attachments ?? []) {
|
|
46
|
+
if (a.kind !== "image") continue;
|
|
47
|
+
const img = await fetchImage(a.url);
|
|
48
|
+
if (img) out.push(img);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
var attachmentLines = (c) => (c.attachments ?? []).length ? ["", "**Attachments:**", ...(c.attachments ?? []).map((a) => `- ${a.kind === "video" ? "\u{1F3AC}" : "\u{1F5BC}"} ${a.name ?? "attachment"} \u2014 ${a.url}`)] : [];
|
|
42
53
|
async function listComments({ status, url }) {
|
|
43
54
|
const q = new URLSearchParams({ projectKey: PROJECT_KEY });
|
|
44
55
|
if (url) q.set("url", url);
|
|
@@ -46,7 +57,7 @@ async function listComments({ status, url }) {
|
|
|
46
57
|
if (status) comments = comments.filter((c) => c.status === status);
|
|
47
58
|
if (!comments.length) return wrap("No comments match.");
|
|
48
59
|
const lines = comments.map(
|
|
49
|
-
(c) => `- [${c.status}] #${c.id} \u2014 ${c.body}
|
|
60
|
+
(c) => `- [${c.status}] #${c.id} \u2014 ${titleOf(c)}: ${c.body}
|
|
50
61
|
\u21B3 ${targetOf(c)} on ${c.url} (by ${c.author.name})`
|
|
51
62
|
);
|
|
52
63
|
return wrap(`${comments.length} comment(s):
|
|
@@ -58,24 +69,28 @@ Use get_comment(id) for the full element context.`);
|
|
|
58
69
|
async function getComment({ id }) {
|
|
59
70
|
const c = await api(`/v1/comments/${encodeURIComponent(id)}`);
|
|
60
71
|
if (c.kind === "free") {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
72
|
+
const text2 = [
|
|
73
|
+
`# Feedback #${c.id} from ${c.author.name} (${c.status})`,
|
|
74
|
+
``,
|
|
75
|
+
`**Title:** ${titleOf(c)}`,
|
|
76
|
+
`**Note:** ${c.body}`,
|
|
77
|
+
`**Page:** ${c.url}`,
|
|
78
|
+
`**Type:** Free note \u2014 a page-level comment, not tied to a specific element.`,
|
|
79
|
+
...attachmentLines(c)
|
|
80
|
+
].join("\n");
|
|
81
|
+
const imgs = await imageBlocks(c.attachments);
|
|
82
|
+
return wrap(text2, ...imgs.map((i) => ({ type: "image", data: i.data, mimeType: i.mimeType })));
|
|
70
83
|
}
|
|
71
84
|
const text = [
|
|
72
85
|
`# Feedback #${c.id} from ${c.author.name} (${c.status})`,
|
|
73
86
|
``,
|
|
87
|
+
`**Title:** ${titleOf(c)}`,
|
|
74
88
|
`**Request:** ${c.body}`,
|
|
75
89
|
`**Page:** ${c.url}`,
|
|
76
90
|
`**Target element:** ${c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : `\`${c.anchor.cssPath}\``}`,
|
|
77
91
|
c.screenshot ? `**Screenshot:** ${c.screenshot}` : ``,
|
|
78
92
|
c.recording ? `**Screen recording (webm):** ${c.recording}` : ``,
|
|
93
|
+
...attachmentLines(c),
|
|
79
94
|
``,
|
|
80
95
|
`## Target element HTML`,
|
|
81
96
|
"```html",
|
|
@@ -100,8 +115,11 @@ async function getComment({ id }) {
|
|
|
100
115
|
`---`,
|
|
101
116
|
`When you've rewritten this UI, call \`propose_change(id: "${c.id}", html, css?, notes?)\` so the dev team sees your modified HTML/CSS in the dashboard.`
|
|
102
117
|
].filter(Boolean).join("\n");
|
|
103
|
-
const
|
|
104
|
-
|
|
118
|
+
const images = [
|
|
119
|
+
...c.screenshot ? [await fetchImage(c.screenshot)] : [],
|
|
120
|
+
...await imageBlocks(c.attachments)
|
|
121
|
+
].filter((i) => i !== null);
|
|
122
|
+
return wrap(text, ...images.map((i) => ({ type: "image", data: i.data, mimeType: i.mimeType })));
|
|
105
123
|
}
|
|
106
124
|
async function updateStatus({ id, status }) {
|
|
107
125
|
await api(`/v1/comments/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify({ status }) });
|
|
@@ -118,7 +136,7 @@ async function proposeChange({ id, html, css, notes }) {
|
|
|
118
136
|
await api(`/v1/comments/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify({ proposal }) });
|
|
119
137
|
return wrap(`Proposal saved for #${id}. The dev team can now review your modified HTML/CSS in the dashboard.`);
|
|
120
138
|
}
|
|
121
|
-
var server = new McpServer({ name: "loupe", version: "0.
|
|
139
|
+
var server = new McpServer({ name: "loupe", version: "0.9.0" });
|
|
122
140
|
server.tool(
|
|
123
141
|
"list_comments",
|
|
124
142
|
"List Loupe product-feedback comments for the project as a task backlog. Use this to see what a PM has flagged, then work through the items.",
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"email": "m.ashraf.saed@gmail.com",
|
|
6
6
|
"url": "https://www.linkedin.com/in/mohamedashrafelsaed/"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.9.0-next.30",
|
|
9
9
|
"description": "MCP server that exposes Loupe comments to Claude Code as an actionable, fully-contextualized backlog.",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"loupe",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"zod": "^3.24.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@loupekit/shared": "0.
|
|
49
|
+
"@loupekit/shared": "0.9.0-next.30"
|
|
50
50
|
}
|
|
51
51
|
}
|