@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +75 -19
  3. 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 = (text) => ({ content: [{ type: "text", text }] });
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
- return wrap(
53
- [
54
- `# Feedback #${c.id} from ${c.author.name} (${c.status})`,
55
- ``,
56
- `**Request:** ${c.body}`,
57
- `**Page:** ${c.url}`,
58
- `**Target element:** ${c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : `\`${c.anchor.cssPath}\``}`,
59
- c.screenshot ? `**Screenshot:** ${c.screenshot}` : ``,
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
- `## Target element HTML`,
92
+ `## Existing proposal (by ${c.proposal.author ?? "unknown"})`,
93
+ c.proposal.notes ? c.proposal.notes : ``,
62
94
  "```html",
63
- c.context.html,
95
+ c.proposal.html,
64
96
  "```",
65
- ``,
66
- `## Computed styles`,
67
- "```json",
68
- JSON.stringify(c.context.styles, null, 2),
69
- "```"
70
- ].filter(Boolean).join("\n")
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
- var server = new McpServer({ name: "loupe", version: "0.6.0" });
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.6.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.6.0"
49
+ "@loupekit/shared": "0.7.0-next.24"
50
50
  }
51
51
  }