@loupekit/mcp 0.8.1 → 0.9.0-next.31

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 (2) hide show
  1. package/dist/index.js +31 -13
  2. 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
- return wrap(
62
- [
63
- `# Feedback #${c.id} from ${c.author.name} (${c.status})`,
64
- ``,
65
- `**Note:** ${c.body}`,
66
- `**Page:** ${c.url}`,
67
- `**Type:** Free note \u2014 a page-level comment, not tied to a specific element (no screenshot).`
68
- ].join("\n")
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 img = c.screenshot ? await fetchImage(c.screenshot) : null;
104
- return img ? wrap(text, { type: "image", data: img.data, mimeType: img.mimeType }) : wrap(text);
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.8.1" });
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.1",
8
+ "version": "0.9.0-next.31",
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.8.1"
49
+ "@loupekit/shared": "0.9.0-next.31"
50
50
  }
51
51
  }