@usercase/mcp 0.2.11-next.3 → 0.2.11

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 CHANGED
@@ -30,6 +30,21 @@ Each repository keeps its own connection, so different sites can go to different
30
30
 
31
31
  Prefer a key? Create one under **Settings → Integrations → API keys** and add `-e USERCASE_API_KEY=eb_sk_...` before `--`. A key you set this way is used first.
32
32
 
33
+ ## Commands
34
+
35
+ | Command | What it does |
36
+ |---|---|
37
+ | `/mcp__usercase__research <url> [count]` | Sends virtual users to your site. [More](#run-a-research) |
38
+ | `/mcp__usercase__install` | Adds the Usercase script to your site. [More](#install-the-script) |
39
+ | `/mcp__usercase__new-research` | Plans a research from your data and code and saves it as a draft. [More](#planning-a-research) |
40
+ | `/mcp__usercase__path <research id>` | Records the route each task is meant to take. [More](#record-a-tasks-expected-path) |
41
+ | `/mcp__usercase__fix <research id> <finding>` | Fixes a finding from a research report, or an issue from Issues, in your code. [More](#fix-a-finding-from-a-report) |
42
+ | `/mcp__usercase__question` | Suggests a question for a precise moment and saves it as a draft. [More](#questions-for-a-precise-moment) |
43
+ | `/mcp__usercase__steps <window id>` | Writes the steps for a question you are making in the dashboard. [More](#questions-for-a-precise-moment) |
44
+ | `/mcp__usercase__connect` | Connects this repository to another project. [More](#connect) |
45
+
46
+ Wherever a command needs an id, the dashboard shows it ready to copy. You can also just ask Claude, for example "research my staging site with 10 virtual users".
47
+
33
48
  ## Install the script
34
49
 
35
50
  The survey cards come from the Usercase script on your site. If it isn't there yet, let Claude Code add it:
@@ -67,6 +82,14 @@ The number is how many people visit in total (default 5). Above 5, Claude works
67
82
 
68
83
  With `unlimited` it keeps going until you press Esc. Every visit uses your Claude usage.
69
84
 
85
+ ### Focus on one part of the site
86
+
87
+ Name a part of the site, and Claude casts only people who come for it:
88
+
89
+ ```
90
+ /mcp__usercase__research https://staging.example.com 5 focus=checkout
91
+ ```
92
+
70
93
  ### Take part in a research
71
94
 
72
95
  Give a research id from your dashboard and the people take part in it instead of exploring freely — they accept its invitation and do its tasks from the study panel, the way invited participants do. The research must be live.
@@ -75,6 +98,8 @@ Give a research id from your dashboard and the people take part in it instead of
75
98
  /mcp__usercase__research https://staging.example.com 10 research=<research id>
76
99
  ```
77
100
 
101
+ If the research is moderated by voice, the people read the moderator's captions and answer out loud, with a voice already on your computer (Windows voices, macOS `say` or `espeak`). No outside voice service is used.
102
+
78
103
  When a visit ends, it is sent to your dashboard (**Visits**). If the upload fails, it is kept in `.usercase/outbox.jsonl` and sent with the next one.
79
104
 
80
105
  ### Sites behind a sign-in
@@ -119,6 +144,26 @@ Claude suggests the moment, the question and its answers, and saves it only afte
119
144
 
120
145
  Claude asks whether you already know what you want to learn. If you don't, it reads your project's findings and where visitors left, and suggests two or three researches, saying which evidence comes from real visitors and which from virtual users. With no data yet, it offers to send a few virtual users first. It then reads your code to write the tasks, their success conditions (the page that means done, or the exact button) and the route each task is meant to take, and shows you all of it at once. On your approval it walks each route in a browser, checks it, and saves the research with the routes — as a draft in your Inbox, where you create it. The routes move into the research when you create it. A draft you discard, or don't create within 30 days, loses its recorded paths and their screenshots.
121
146
 
147
+ ## Fix a finding from a report
148
+
149
+ A research's **Report** lists findings: problems people hit, each backed by what they did, what the screen showed and what they wrote. Each finding has a **Fix with your agent** command:
150
+
151
+ ```
152
+ /mcp__usercase__fix <research id> <finding, e.g. F1>
153
+ ```
154
+
155
+ Add `real` for the report of real people; without it, the report of virtual users is used. Claude reads the finding and any earlier fixes of it, finds the code behind it, and tells you the change it proposes before touching anything. After you approve, it makes the change, runs your checks and records the fix on the finding. It doesn't commit, push or open a pull request unless you ask.
156
+
157
+ Once the change is deployed, mark it **Live on site** on the finding: the report compares people before and after the fix only from then on. To check it with virtual users, run the same tasks again with `research=<research id>`.
158
+
159
+ Issues from your questions' answers have the same button in **Issues**:
160
+
161
+ ```
162
+ /mcp__usercase__fix issue <issue id>
163
+ ```
164
+
165
+ Claude reads the question, what people chose and wrote, and any other issues with the same cause, then works the same way. Once the change is deployed, mark the issue reviewed; the question keeps running, so later answers show whether people still hit it.
166
+
122
167
  ## What the visitors will and won't do
123
168
 
124
169
  - They identify themselves on every request (see below), and their answers are counted separately from real visitors. This cannot be turned off.
@@ -0,0 +1,40 @@
1
+ # Fix an issue found from your visitors' answers
2
+
3
+ Issue: {{issue}}
4
+
5
+ Usercase asks short questions on this site and groups the answers into issues — problems on the site, each backed by the question, what people chose and what they wrote. You fix one issue in this repository's code.
6
+
7
+ ## 1. Read the issue
8
+
9
+ Call `issue_read` with the issue id. If no issue id is given above, call `issue_list` first, show the user the open issues (title, where, whether it is a clear fix or the cause is unclear, and whether someone is already fixing it), and ask which one to fix.
10
+
11
+ Read all of it: the question and the page it was asked on, what people chose, what they wrote, the AI's reading of it and the suggested next step. The suggestion was written without seeing the code; treat it as a direction, not a spec.
12
+
13
+ If the cause is marked unclear, say so to the user: the answers show the problem but not why. Propose the change only if the code makes the cause clear; otherwise suggest running a research on it instead ({{research_cmd}}).
14
+
15
+ If it lists other issues with the same cause, tell the user — one change may fix them together.
16
+
17
+ If it lists earlier fixes, read what each changed. Do not repeat an approach that is already in place; say how your change differs. If an earlier fix is probably not deployed yet, tell the user — they may only need to deploy it.
18
+
19
+ ## 2. Find the code
20
+
21
+ From the page, the question and what people wrote, find the code behind it: routes, the page components, the elements people talk about, the text shown. Confirm you found the right place by matching what people mention (a section, a button, a label) to the code.
22
+
23
+ ## 3. Propose the change — before changing anything
24
+
25
+ Tell the user, briefly:
26
+ - what in the code causes the problem, with file paths,
27
+ - the change you propose and why it addresses what people chose and wrote, and
28
+ - anything the issue suggests that you would not do, and why (it would break something, it needs a product decision, the code already does it).
29
+
30
+ Keep the change as small as solves the problem. Do not redesign unrelated parts. Wait for the user's approval or corrections.
31
+
32
+ ## 4. Change the code
33
+
34
+ When the user approves, call `issue_fix` with `status` `fixing` and the files you will change. Then make the change. Follow the repository's own conventions, and run its checks (type check, lint, tests) if it has them.
35
+
36
+ ## 5. Record the fix
37
+
38
+ When the change is done, call `issue_fix` with `status` `fixed`, the files changed, a one-sentence summary written for the team (what a visitor now sees differently), and the pull request link if you opened one. Do not commit, push or open a pull request unless the user asks.
39
+
40
+ Then tell the user that the issue shows as fixed in Issues, and that once the change is deployed they mark it reviewed there. The question keeps running, so later answers show whether people still hit it.
package/agents/fix.md CHANGED
@@ -12,6 +12,8 @@ Call `report_finding` with the research id, the finding key and the report. If n
12
12
 
13
13
  Read all of it: the title and why, who it hit, the screen checks (what the page really showed — these come from the recordings, not from opinions), the paths people took, the groups of what they wrote, and the suggested changes. The suggested changes were written without seeing the code; treat them as a direction, not a spec.
14
14
 
15
+ If it lists earlier fixes of this finding, read what each changed and how it went. Do not repeat an approach that made things worse or changed nothing; say how your change differs. If an earlier fix is not live yet or probably not deployed, tell the user — they may only need to deploy it.
16
+
15
17
  ## 2. Find the code
16
18
 
17
19
  From the pages, buttons and texts in the evidence, find the code behind them: routes, the page components, the button and its click handler, the text shown. Confirm you found the right place by matching the exact text on screen (a button label, a heading) to the code.
@@ -31,6 +33,8 @@ When the user approves, call `report_fix` with `status` `fixing` and the files y
31
33
 
32
34
  ## 5. Record the fix
33
35
 
34
- When the change is done, call `report_fix` with `status` `fixed`, the files changed, a one-sentence summary written for the team (what a visitor now sees differently), and the pull request link if you opened one. Do not commit, push or open a pull request unless the user asks.
36
+ When the change is done, call `report_fix` with `status` `fixed`, the files changed, a one-sentence summary written for the team (what a visitor now sees differently — the next report and the next fix read it), and the pull request link if you opened one. Do not commit, push or open a pull request unless the user asks.
37
+
38
+ The report compares people only after the fix is live on the site. Tell the user to mark it **Live on site** on the finding once it is deployed; when a new version of their site shows up in sessions, the report also asks whether this fix is in it.
35
39
 
36
40
  Then tell the user that the finding shows as fixed in the report, and how to check it worked: {{check}}
package/mcp/hub.js CHANGED
@@ -721,9 +721,15 @@ class Hub {
721
721
  }
722
722
  if (p.kind === "timeout") {
723
723
  const skip = decision.action === "choose" && decision.choiceIndex === 1;
724
- await study.press(page, skip ? "timeoutSkip" : "keepGoing").catch(() => {});
724
+ // 눌리지 않았으면 그대로 알린다 — 전에는 실패를 넘기고 "골랐다"고 해서 같은 창에 갇혔다(11/05)
725
+ let moved = null;
726
+ try {
727
+ moved = await study.press(page, skip ? "timeoutSkip" : "keepGoing");
728
+ } catch (e) {
729
+ return [`You pressed "${skip ? "Skip" : "Continue"}", but it did not go through: ${String(e.message).split("\n")[0]}. The study is still asking.`, "", await this.screen(rec)].join("\n");
730
+ }
725
731
  if (skip) st.skipped.push(p.task);
726
- said = skip ? `You chose to skip the task "${p.task}".` : "You chose to keep trying.";
732
+ said = (skip ? `You chose to skip the task "${p.task}".` : "You chose to keep trying.") + (moved ? ` (${moved} was covering the study panel, so you dragged the panel aside first.)` : "");
727
733
  } else {
728
734
  const q = p.q;
729
735
  const ans = decision.action === "choose" ? { index: decision.choiceIndex } : decision.action === "other" ? { text: decision.otherText } : {};
package/mcp/path.js CHANGED
@@ -91,7 +91,9 @@ function describeInPage(n, { sel: pickSel, lift = false }) {
91
91
  } catch (e) {}
92
92
  // SDK 는 id 를 이스케이프 없이 `#id` 로 쓴다 — 평범한 id 만(React useId 의 ":r1:" 같은 건 SDK 에서 오류가 난다)
93
93
  const ids = pick.id && /^[A-Za-z][\w-]*$/.test(pick.id) && document.querySelectorAll("#" + pick.id).length === 1 ? [pick.id] : [];
94
- const label = (pick.getAttribute("aria-label") || pick.innerText || pick.value || pick.getAttribute("title") || "").replace(/\s+/g, " ").trim().slice(0, 80);
94
+ // 입력칸에 친 값은 이름으로 안 쓴다(방문자 정보·비밀번호가 들어간다). submit·button 의 value 는 버튼 글자라 쓴다.
95
+ // 아래 path_check 의 label 과 같은 식이어야 한다 — 다르면 같은 요소를 "이름이 다르다"로 본다
96
+ const label = (pick.getAttribute("aria-label") || pick.innerText || (pick.tagName === "INPUT" && /^(submit|button|reset)$/i.test(pick.type) ? pick.value : "") || pick.getAttribute("title") || pick.getAttribute("placeholder") || "").replace(/\s+/g, " ").trim().slice(0, 80);
95
97
  return { xpath: xp(pick), classes, ids, label, tag: pick.tagName.toLowerCase() };
96
98
  }
97
99
 
@@ -366,7 +368,7 @@ class PathRecorder {
366
368
  if (!ok) break;
367
369
  const h = (await page.evaluateHandle(byXPathInPage, a.xpath)).asElement();
368
370
  const visible = h ? await h.isVisible().catch(() => false) : false;
369
- const label = h ? await h.evaluate((n) => (n.getAttribute("aria-label") || n.innerText || n.value || n.getAttribute("title") || "").replace(/\s+/g, " ").trim().slice(0, 80)) : "";
371
+ const label = h ? await h.evaluate((n) => (n.getAttribute("aria-label") || n.innerText || (n.tagName === "INPUT" && /^(submit|button|reset)$/i.test(n.type) ? n.value : "") || n.getAttribute("title") || n.getAttribute("placeholder") || "").replace(/\s+/g, " ").trim().slice(0, 80)) : "";
370
372
  const no = a.step !== null ? `${a.step + 1}. ` : " ";
371
373
  if (!h || !visible) {
372
374
  lines.push(`✗ ${no}${a.do} "${a.label || a.text || ""}" — ${h ? "found but not visible" : "not found at its recorded position"} (${a.xpath})`);
package/mcp/report.js CHANGED
@@ -297,6 +297,10 @@ const rq = (audience) => (audience === "real" ? "?audience=real" : "?audience=vi
297
297
  const getReport = (researchId, audience) => keyed("GET", `/agent/researches/${encodeURIComponent(researchId)}/report${rq(audience)}`);
298
298
  const getFinding = (researchId, key, audience) => keyed("GET", `/agent/researches/${encodeURIComponent(researchId)}/report/findings/${encodeURIComponent(key)}${rq(audience)}`);
299
299
  const saveFix = (researchId, key, body) => keyed("PUT", `/agent/researches/${encodeURIComponent(researchId)}/report/findings/${encodeURIComponent(key)}/fix`, body);
300
+ /** Issues(fix issue) — 열린 문제 목록, 문제 하나와 근거, 고친 기록 */
301
+ const getIssues = () => keyed("GET", "/agent/issues");
302
+ const getIssue = (id) => keyed("GET", `/agent/issues/${encodeURIComponent(id)}`);
303
+ const saveIssueFix = (id, body) => keyed("PUT", `/agent/issues/${encodeURIComponent(id)}/fix`, body);
300
304
  /** 대시보드 주소. API 주소에서 /koa 를 뗀다 */
301
305
  const dashboardUrl = (p) => apiBase().replace(/\/koa$/, "") + p;
302
306
 
@@ -323,5 +327,8 @@ module.exports = {
323
327
  getReport,
324
328
  getFinding,
325
329
  saveFix,
330
+ getIssues,
331
+ getIssue,
332
+ saveIssueFix,
326
333
  dashboardUrl,
327
334
  };
package/mcp/server.js CHANGED
@@ -609,6 +609,15 @@ server.registerTool(
609
609
  if (e.words.length) L.push("", "What people wrote (grouped by meaning, with examples):", ...e.words.map((w) => `- ${w.text}`));
610
610
  if (e.numbers.length) L.push("", "Numbers:", ...e.numbers.map((x) => `- ${x}`));
611
611
  L.push("", "Suggested changes (written without seeing the code):", ...f.changes.map((c, i) => `${i + 1}. ${c}`), `Check after the fix: ${f.measure}`);
612
+ // 지난 고침 — 무엇을 바꿨고 어땠나. 같은 방법을 되풀이하지 않게
613
+ const VERDICT = { better: "better", worse: "worse", mixed: "mixed", same: "no change", waiting: "not compared yet", not_deployed: "probably not deployed — sessions after it still ran the old version" };
614
+ if (r.history && r.history.length) {
615
+ L.push("", "Earlier fixes of this finding (each compared with the version right before it):");
616
+ for (const h of r.history) {
617
+ L.push(`- Fix ${h.attempt} by ${h.by}: ${h.summary || "(no note)"} — ${h.liveAt ? `live since ${String(h.liveAt).slice(0, 16).replace("T", " ")}` : "not live yet"}, ${VERDICT[h.verdict] || h.verdict}`);
618
+ for (const row of h.rows || []) L.push(` · ${row.label}: ${row.before[0]}/${row.before[1]} → ${row.after[0]}/${row.after[1]}`);
619
+ }
620
+ }
612
621
  if (r.sessionsUrl) L.push("", `Watch these sessions: ${r.sessionsUrl}`);
613
622
  L.push(`Report: ${r.reportUrl}`);
614
623
  return text(L.join("\n"));
@@ -619,7 +628,7 @@ server.registerTool(
619
628
  "report_fix",
620
629
  {
621
630
  description:
622
- "Record on the research report that you are fixing a finding (status fixing, once the user approved the change) or have fixed it (status fixed, with the files, a one-sentence summary for the team, and the pull request link if any). The report shows it on that finding.",
631
+ "Record on the research report that you are fixing a finding (status fixing, once the user approved the change) or have fixed it (status fixed, with the files, a one-sentence summary for the team, and the pull request link if any). If the last fix of this finding is already live on the site, this starts the next fix (fix 2, 3 …); otherwise it adds to the current one. The report compares people after each fix goes live with the version right before it.",
623
632
  inputSchema: {
624
633
  research: z.string().describe("The research id"),
625
634
  finding: z.string().describe("Finding key, e.g. F1"),
@@ -633,7 +642,86 @@ server.registerTool(
633
642
  wrap(async ({ research, finding, audience, status, files, summary, prUrl }) => {
634
643
  const agent = CODEX ? "Codex" : OTHER ? null : "Claude Code";
635
644
  const r = await report.saveFix(research, finding, { audience: audience || "virtual", status, agent, files, summary, prUrl });
636
- return text(`${r.key} is marked ${r.status} on the report: ${r.reportUrl}`);
645
+ return text(`${r.key} fix ${r.attempt} is marked ${r.status} on the report: ${r.reportUrl}`);
646
+ }),
647
+ );
648
+
649
+ // ── Issues 의 문제 고치기(fix issue) ──
650
+ const issueFixLine = (f) => (f ? ` — ${f.status === "fixed" ? "fixed" : "being fixed"}${f.agent ? ` by ${f.agent}` : ""}` : "");
651
+
652
+ server.registerTool(
653
+ "issue_list",
654
+ {
655
+ description: "List the open issues of the Usercase project (problems found from visitors' answers to questions on the site): id, title, where, whether it is a clear fix, and whether one is being fixed. Use when the fix command has no issue id.",
656
+ inputSchema: {},
657
+ },
658
+ wrap(async () => {
659
+ const r = await report.getIssues();
660
+ if (!r.issues.length) return text("No open issues in this project.");
661
+ return text(
662
+ [
663
+ ...r.issues.map(
664
+ (i) =>
665
+ `${i.id} · ${i.title} — ${i.page || "any page"}, ${i.answers} ${i.from === "virtual" ? "virtual users" : "answers"}, ${i.nextStep === "fix" ? "clear fix" : "cause unclear"}${i.status === "acknowledged" ? ", reviewed" : ""}${issueFixLine(i.fix)}`,
666
+ ),
667
+ "",
668
+ `Issues: ${r.url}`,
669
+ ].join("\n"),
670
+ );
671
+ }),
672
+ );
673
+
674
+ server.registerTool(
675
+ "issue_read",
676
+ {
677
+ description:
678
+ "Read one issue of the Usercase project with all its evidence: the question and page, what people chose, what they wrote, the AI's reading, other issues with the same cause, and earlier fixes. Read it before looking for the code.",
679
+ inputSchema: { issue: z.string().describe("The issue id, as in the dashboard command") },
680
+ },
681
+ wrap(async ({ issue }) => {
682
+ const r = await report.getIssue(String(issue).trim());
683
+ const i = r.issue;
684
+ const e = r.evidence;
685
+ const L = [
686
+ `${i.title}${issueFixLine(i.fix)}`,
687
+ `From ${i.answers} ${i.from === "virtual" ? "virtual users' answers" : "visitors' answers"} · ${i.nextStep === "fix" ? "clear fix" : "cause unclear — the answers show the problem, not why"}`,
688
+ "",
689
+ `Question: "${r.question.text}" (asked on ${r.question.page || "any page"})`,
690
+ "",
691
+ `What the answers show: ${i.observation}`,
692
+ `Likely why: ${i.hypothesis}`,
693
+ ];
694
+ if (i.nextStepNote) L.push(`Suggested next step (written without seeing the code): ${i.nextStepNote}`);
695
+ if (e.distribution.length) L.push("", "What people chose:", ...e.distribution.map((d) => `- ${d.label}: ${d.n}${e.focus.includes(d.label) ? " ← this issue" : ""}`));
696
+ if (e.words.length) L.push("", "What people wrote:", ...e.words.map((w) => `- "${w.text}"${w.n > 1 ? ` (${w.n} people)` : ""}${w.page ? ` (on ${w.page}${w.device ? `, ${w.device}` : ""})` : ""}`));
697
+ if (e.pages.length) L.push("", `Pages: ${e.pages.map((p) => `${p.page} (${p.n})`).join(", ")}`);
698
+ if (r.cause) L.push("", `Same cause as other issues — "${r.cause.title}": ${r.cause.summary}`, ...r.cause.others.map((o) => `- ${o.id} · ${o.title}${o.page ? ` (${o.page})` : ""}`));
699
+ if (r.history.length) {
700
+ L.push("", "Earlier fixes of this issue:");
701
+ for (const h of r.history) L.push(`- Fix ${h.attempt} by ${h.by} (${h.status}): ${h.summary || "(no note)"}${h.files.length ? ` — ${h.files.join(", ")}` : ""}`);
702
+ }
703
+ L.push("", `Issue: ${r.url}`);
704
+ return text(L.join("\n"));
705
+ }),
706
+ );
707
+
708
+ server.registerTool(
709
+ "issue_fix",
710
+ {
711
+ description:
712
+ "Record on a Usercase issue that you are fixing it (status fixing, once the user approved the change) or have fixed it (status fixed, with the files, a one-sentence summary for the team, and the pull request link if any). After a finished fix, starting again records the next fix.",
713
+ inputSchema: {
714
+ issue: z.string().describe("The issue id"),
715
+ status: z.enum(["fixing", "fixed"]),
716
+ files: z.array(z.string()).optional().describe("Repository paths of the files changed (or to be changed)"),
717
+ summary: z.string().optional().describe("One sentence for the team: what visitors now see differently"),
718
+ prUrl: z.string().optional().describe("Pull request link, if one was opened"),
719
+ },
720
+ },
721
+ wrap(async ({ issue, status, files, summary, prUrl }) => {
722
+ const agent = CODEX ? "Codex" : OTHER ? null : "Claude Code";
723
+ const r = await report.saveIssueFix(String(issue).trim(), { status, agent, files, summary, prUrl });
724
+ return text(`"${r.title}" fix ${r.attempt} is marked ${r.status}: ${r.url}`);
637
725
  }),
638
726
  );
639
727
 
@@ -1018,6 +1106,14 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1018
1106
  */
1019
1107
  function byName(args = {}, schema = {}) {
1020
1108
  const names = Object.keys(schema || {});
1109
+ // fix 의 `issue <id>` — 띄어쓰기로 research="issue", finding="<id>" 로 나뉘어 온다. 그대로 두면 아래에서 id 가 research 로 간다
1110
+ if (names.includes("issue")) {
1111
+ const vals = Object.values(args || {}).map((v) => (v == null ? "" : String(v).trim()));
1112
+ if (vals.some((v) => /^issue$/i.test(v))) {
1113
+ const id = vals.find((v) => UUID.test(v));
1114
+ return id ? { issue: id } : { research: "issue" };
1115
+ }
1116
+ }
1021
1117
  const out = {};
1022
1118
  const loose = [];
1023
1119
  for (const [k, raw] of Object.entries(args || {})) {
@@ -1130,10 +1226,18 @@ const COMMANDS = {
1130
1226
  },
1131
1227
  // 리서치 Report 의 발견 하나를 고친다 — 대시보드 Report 의 "Fix with your agent" 명령
1132
1228
  fix: {
1133
- description: "Fix a finding from a Usercase research report in this repository's code",
1134
- argsSchema: { research: z.string().optional(), finding: z.string().optional(), audience: z.string().optional() },
1135
- body: ({ research, finding, audience }) =>
1136
- agentDoc("fix.md", {
1229
+ description: "Fix a finding from a Usercase research report, or an issue from Issues, in this repository's code",
1230
+ argsSchema: {
1231
+ research: z.string().optional(),
1232
+ finding: z.string().optional(),
1233
+ audience: z.string().optional(),
1234
+ issue: z.string().optional().describe("An issue id from the dashboard's Issues — fixes that issue instead of a report finding"),
1235
+ },
1236
+ body: ({ research, finding, audience, issue }) =>
1237
+ // Issues 의 문제 — `fix issue <id>`. 이름 없이 issue 만 오면 목록부터
1238
+ issue || String(research || "").trim().toLowerCase() === "issue"
1239
+ ? agentDoc("fix-issue.md", { issue: String(issue || "").trim() || "(not given — list the open issues and ask)", research_cmd: cmd("research") })
1240
+ : agentDoc("fix.md", {
1137
1241
  research: research || "(ask the user — the dashboard Report shows the command)",
1138
1242
  finding: finding ? String(finding).toUpperCase() : "(not given — list the findings and ask)",
1139
1243
  audience: audience === "real" ? "real (the report of real people)" : "virtual (the report of virtual users)",
@@ -1155,7 +1259,7 @@ if (PLUGIN || CODEX || OTHER) {
1155
1259
  "instructions",
1156
1260
  {
1157
1261
  description:
1158
- "The step-by-step instructions for a Usercase command (research, install, path, question, new-research, steps, connect, fix). The plugin's /usercase:* commands call this; follow what it returns as the user's request.",
1262
+ "The step-by-step instructions for a Usercase command (research, install, path, question, new-research, steps, connect, fix — a report finding, or an issue with `issue`). The plugin's /usercase:* commands call this; follow what it returns as the user's request.",
1159
1263
  inputSchema: {
1160
1264
  command: z.enum(Object.keys(COMMANDS)),
1161
1265
  ...COMMANDS.research.argsSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usercase/mcp",
3
- "version": "0.2.11-next.3",
3
+ "version": "0.2.11",
4
4
  "description": "Virtual users that browse your website and tell you what they thought — an MCP server for Claude Code",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://usercase.io",
@@ -226,11 +226,68 @@ async function movePanel(page, away) {
226
226
  await drag(tx, ty);
227
227
  }
228
228
 
229
- /** 위젯 단추를 누른다 */
229
+ /**
230
+ * 패널 안 요소 위에 페이지의 다른 무언가가 올라와 있나 — 그 요소 한가운데 점의 맨 위 요소로 본다.
231
+ * 고객 사이트의 설문 팝업 · 채팅 · 쿠키 배너가 z-index 최대값으로 패널을 덮어 클릭을 가져갔다(11/05 bluetags Maze).
232
+ * 특정 위젯 이름을 정해 두지 않는다 — 덮은 것의 모양(태그 · 주소의 도메인 · 글 · 크기)으로 설명한다.
233
+ * @returns 안 덮였으면 null, 덮였으면 { at: {x, y} 덮인 점, what: 사람이 읽을 설명 }
234
+ */
235
+ async function coveredBy(locator) {
236
+ return locator
237
+ .first()
238
+ .evaluate((el) => {
239
+ const r = el.getBoundingClientRect();
240
+ if (!r.width || !r.height) return null;
241
+ const x = r.x + r.width / 2;
242
+ const y = r.y + r.height / 2;
243
+ const top = document.elementFromPoint(x, y);
244
+ const host = document.getElementById("eb-plugin");
245
+ if (!top || top === host || (host && host.contains(top))) return null;
246
+ // 덮은 덩어리 — 위로 올라가 떠 있는(fixed · absolute) 상자까지
247
+ let box = top;
248
+ for (let i = 0; i < 8 && box.parentElement && box.parentElement !== document.body; i++) {
249
+ const st = getComputedStyle(box);
250
+ if (st.position === "fixed" || st.position === "absolute") break;
251
+ box = box.parentElement;
252
+ }
253
+ const br = box.getBoundingClientRect();
254
+ const tag = box.tagName.toLowerCase();
255
+ let from = "";
256
+ if (tag === "iframe") {
257
+ try {
258
+ from = new URL(box.src).host;
259
+ } catch (e) {}
260
+ }
261
+ const words = (tag === "iframe" ? box.getAttribute("title") || "" : box.innerText || "").replace(/\s+/g, " ").trim().slice(0, 70);
262
+ const kind = tag === "iframe" ? "an embedded frame" + (from ? " from " + from : "") : "a box";
263
+ const what = `${kind}${words ? ` showing "${words}"` : ""}, ${Math.round(br.width)}×${Math.round(br.height)} px`;
264
+ return { at: { x: Math.round(x), y: Math.round(y) }, what };
265
+ })
266
+ .catch(() => null);
267
+ }
268
+
269
+ /**
270
+ * 패널 안 무언가를 누른다. 다른 무언가가 덮고 있으면 패널을 그 반대쪽으로 끌어 비키고 다시 본다 — 사람이 하듯이.
271
+ * 그래도 덮이면 던진다(조용히 넘기지 않는다 — 넘기면 방문자는 눌렀다고 믿고 같은 창에 갇힌다).
272
+ * @returns 비켜서 눌렀으면 덮었던 것의 설명, 아니면 null
273
+ */
274
+ async function clickInPanel(page, locator) {
275
+ const cover = await coveredBy(locator);
276
+ if (cover) {
277
+ await movePanel(page, cover.at).catch(() => {});
278
+ await sleep(300);
279
+ const still = await coveredBy(locator);
280
+ if (still) throw new Error(`something else on the page is on top of it — ${still.what} — so the click does not reach the study panel`);
281
+ }
282
+ await locator.first().click({ timeout: 5000 });
283
+ return cover ? cover.what : null;
284
+ }
285
+
286
+ /** 위젯 단추를 누른다. @returns 덮고 있던 것을 비켜서 눌렀으면 그 설명 */
230
287
  async function press(page, key) {
231
288
  const sel = PRESS[key];
232
289
  if (!sel) throw new Error(`Unknown study button ${key}`);
233
- await page.locator(sel).first().click({ timeout: 5000 });
290
+ return clickInPanel(page, page.locator(sel));
234
291
  }
235
292
 
236
293
  /**
@@ -242,11 +299,11 @@ async function answerQuestion(page, q, answer) {
242
299
  const root = page.locator("#eb-plugin .eb-frost-survey");
243
300
  if (Number.isInteger(answer.index) && answer.index >= 0) {
244
301
  const sel = q.kind === "choice" ? ".eb-sq-choice:not(:has(input))" : q.kind === "scale" ? ".eb-sq-likert-btn" : ".eb-sq-yesno-btn";
245
- await root.locator(sel).nth(answer.index).click({ timeout: 5000 });
302
+ await clickInPanel(page, root.locator(sel).nth(answer.index));
246
303
  } else if (answer.text) {
247
304
  const box = q.kind === "choice" ? root.locator("input.eb-sq-choice-other") : root.locator("textarea.eb-sq-textarea, input.eb-sq-input").first();
248
305
  if (!(await box.count())) return "there is no box to type into for this question — pick one of the options";
249
- await box.click({ timeout: 5000 }).catch(() => {});
306
+ await clickInPanel(page, box);
250
307
  await box.fill(String(answer.text).slice(0, q.multiline ? 1000 : 200), { timeout: 5000 });
251
308
  }
252
309
  await sleep(400);
@@ -257,4 +314,4 @@ async function answerQuestion(page, q, answer) {
257
314
  return null;
258
315
  }
259
316
 
260
- module.exports = { readStudy, waitState, join, press, movePanel, answerQuestion, isSurveyUrl };
317
+ module.exports = { readStudy, waitState, join, press, movePanel, answerQuestion, isSurveyUrl, coveredBy };