@loupekit/mcp 0.6.0 → 0.7.0-next.24
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 +2 -1
- package/dist/index.js +75 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -52,7 +52,8 @@ element, its state, and the page.
|
|
|
52
52
|
| Tool | Description |
|
|
53
53
|
| --- | --- |
|
|
54
54
|
| `list_comments(status?, url?)` | List comments, optionally filtered by status (`open` / `in_progress` / `done`) or page URL. |
|
|
55
|
-
| `get_comment(id)` | The full Claude-ready package for one comment: request + element HTML + computed styles + screenshot URL. |
|
|
55
|
+
| `get_comment(id)` | The full Claude-ready package for one comment: request + element HTML + computed styles + the **screenshot as an image** + any screen-recording URL. |
|
|
56
|
+
| `propose_change(id, html, css?, notes?)` | Write your **modified HTML/CSS** back onto the comment. The dashboard renders it as code plus a live before/after preview for the dev team. |
|
|
56
57
|
| `update_status(id, status)` | Move a comment across the workflow (`open` → `in_progress` → `done`) so triage state stays in sync. |
|
|
57
58
|
|
|
58
59
|
## Install
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pathToFileURL } from "url";
|
|
|
7
7
|
import { realpathSync } from "fs";
|
|
8
8
|
import { argv } from "process";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { Buffer } from "buffer";
|
|
10
11
|
var API = (process.env.LOUPE_API || "http://localhost:8787").replace(/\/$/, "");
|
|
11
12
|
var PROJECT_KEY = process.env.LOUPE_PROJECT_KEY || "pk_demo_acme";
|
|
12
13
|
var ADMIN = process.env.LOUPE_ADMIN_KEY || "";
|
|
@@ -18,7 +19,25 @@ async function api(path, init) {
|
|
|
18
19
|
if (!res.ok) throw new Error(`${init?.method || "GET"} ${path} \u2192 ${res.status}`);
|
|
19
20
|
return res.status === 204 ? null : res.json();
|
|
20
21
|
}
|
|
21
|
-
var wrap = (
|
|
22
|
+
var wrap = (...parts) => ({
|
|
23
|
+
content: parts.map((p) => typeof p === "string" ? { type: "text", text: p } : p)
|
|
24
|
+
});
|
|
25
|
+
async function fetchImage(url) {
|
|
26
|
+
try {
|
|
27
|
+
if (url.startsWith("data:")) {
|
|
28
|
+
const m = /^data:([^;,]+)[^,]*,(.*)$/s.exec(url);
|
|
29
|
+
if (!m) return null;
|
|
30
|
+
return { mimeType: m[1] || "image/png", data: m[2].replace(/^base64,/, "") };
|
|
31
|
+
}
|
|
32
|
+
const res = await fetch(url);
|
|
33
|
+
if (!res.ok) return null;
|
|
34
|
+
const mimeType = res.headers.get("content-type") || "image/png";
|
|
35
|
+
if (!mimeType.startsWith("image/")) return null;
|
|
36
|
+
return { mimeType, data: Buffer.from(await res.arrayBuffer()).toString("base64") };
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
22
41
|
var targetOf = (c) => c.kind === "free" ? "page-level note" : c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : c.anchor.cssPath;
|
|
23
42
|
async function listComments({ status, url }) {
|
|
24
43
|
const q = new URLSearchParams({ projectKey: PROJECT_KEY });
|
|
@@ -49,32 +68,57 @@ async function getComment({ id }) {
|
|
|
49
68
|
].join("\n")
|
|
50
69
|
);
|
|
51
70
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
const text = [
|
|
72
|
+
`# Feedback #${c.id} from ${c.author.name} (${c.status})`,
|
|
73
|
+
``,
|
|
74
|
+
`**Request:** ${c.body}`,
|
|
75
|
+
`**Page:** ${c.url}`,
|
|
76
|
+
`**Target element:** ${c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : `\`${c.anchor.cssPath}\``}`,
|
|
77
|
+
c.screenshot ? `**Screenshot:** ${c.screenshot}` : ``,
|
|
78
|
+
c.recording ? `**Screen recording (webm):** ${c.recording}` : ``,
|
|
79
|
+
``,
|
|
80
|
+
`## Target element HTML`,
|
|
81
|
+
"```html",
|
|
82
|
+
c.context.html,
|
|
83
|
+
"```",
|
|
84
|
+
``,
|
|
85
|
+
`## Computed styles`,
|
|
86
|
+
"```json",
|
|
87
|
+
JSON.stringify(c.context.styles, null, 2),
|
|
88
|
+
"```",
|
|
89
|
+
// Surface an existing proposal so Claude can iterate rather than start over.
|
|
90
|
+
...c.proposal ? [
|
|
60
91
|
``,
|
|
61
|
-
`##
|
|
92
|
+
`## Existing proposal (by ${c.proposal.author ?? "unknown"})`,
|
|
93
|
+
c.proposal.notes ? c.proposal.notes : ``,
|
|
62
94
|
"```html",
|
|
63
|
-
c.
|
|
95
|
+
c.proposal.html,
|
|
64
96
|
"```",
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
97
|
+
...c.proposal.css ? ["```css", c.proposal.css, "```"] : []
|
|
98
|
+
] : [],
|
|
99
|
+
``,
|
|
100
|
+
`---`,
|
|
101
|
+
`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
|
+
].filter(Boolean).join("\n");
|
|
103
|
+
const img = c.screenshot ? await fetchImage(c.screenshot) : null;
|
|
104
|
+
return img ? wrap(text, { type: "image", data: img.data, mimeType: img.mimeType }) : wrap(text);
|
|
72
105
|
}
|
|
73
106
|
async function updateStatus({ id, status }) {
|
|
74
107
|
await api(`/v1/comments/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify({ status }) });
|
|
75
108
|
return wrap(`#${id} \u2192 ${status}`);
|
|
76
109
|
}
|
|
77
|
-
|
|
110
|
+
async function proposeChange({ id, html, css, notes }) {
|
|
111
|
+
const proposal = {
|
|
112
|
+
html,
|
|
113
|
+
css,
|
|
114
|
+
notes,
|
|
115
|
+
author: "Claude Code via MCP",
|
|
116
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
117
|
+
};
|
|
118
|
+
await api(`/v1/comments/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify({ proposal }) });
|
|
119
|
+
return wrap(`Proposal saved for #${id}. The dev team can now review your modified HTML/CSS in the dashboard.`);
|
|
120
|
+
}
|
|
121
|
+
var server = new McpServer({ name: "loupe", version: "0.7.0" });
|
|
78
122
|
server.tool(
|
|
79
123
|
"list_comments",
|
|
80
124
|
"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.",
|
|
@@ -96,6 +140,17 @@ server.tool(
|
|
|
96
140
|
{ id: z.string(), status: z.enum(["open", "in_progress", "done"]) },
|
|
97
141
|
updateStatus
|
|
98
142
|
);
|
|
143
|
+
server.tool(
|
|
144
|
+
"propose_change",
|
|
145
|
+
"Submit the modified UI for a comment: the rewritten HTML (and optional CSS) that resolves the PM's request. This stores your proposal on the comment so the dev team can review the code and a live preview in the dashboard. Use get_comment first to see the original element, its computed styles, and the screenshot.",
|
|
146
|
+
{
|
|
147
|
+
id: z.string().describe("The comment id from list_comments."),
|
|
148
|
+
html: z.string().describe("The modified element markup that implements the requested change."),
|
|
149
|
+
css: z.string().optional().describe("Accompanying CSS. Omit if the styling is inlined in the HTML."),
|
|
150
|
+
notes: z.string().optional().describe("A short explanation of what you changed and why.")
|
|
151
|
+
},
|
|
152
|
+
proposeChange
|
|
153
|
+
);
|
|
99
154
|
function isEntrypoint() {
|
|
100
155
|
try {
|
|
101
156
|
return import.meta.url === pathToFileURL(realpathSync(argv[1] ?? "")).href;
|
|
@@ -110,5 +165,6 @@ if (isEntrypoint()) {
|
|
|
110
165
|
export {
|
|
111
166
|
getComment,
|
|
112
167
|
listComments,
|
|
168
|
+
proposeChange,
|
|
113
169
|
updateStatus
|
|
114
170
|
};
|
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.7.0-next.24",
|
|
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.7.0-next.24"
|
|
50
50
|
}
|
|
51
51
|
}
|