@usercase/mcp 0.2.1 → 0.2.2-next.0

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.
@@ -0,0 +1,26 @@
1
+ ## This run has a focus: {{focus}}
2
+
3
+ The user wants this run to be about one part of the site — "{{focus}}" (it may name more than one feature). Everything below still applies, with these changes:
4
+
5
+ **Step 2 — pin down what the focus means.** After you build the feature map, find which features, pages and routes "{{focus}}" covers. Before casting anyone, tell the user in one or two lines what you understood (e.g. "the calendar = /calendar and the event detail popup") and wait for them to confirm. If you cannot find it on the map, or it could mean several things, ask — do not guess from the word alone.
6
+
7
+ Then check what the focus needs before anyone is cast:
8
+ - If it is only reachable when signed in, demo account sign-in is required, not optional — say so, and if signed-in visits turn out to be impossible, tell the user the focus cannot really be reached and ask whether to go on.
9
+ - If it is mostly writing to the site (comments, posting, saving, sign-up) and writes are not sent, tell the user when you ask about writes: with "not sent", visitors reach it but it is never really tried.
10
+
11
+ **Step 3 — cast for the focus instead of spreading over the whole map.** Every person comes with a reason that makes the focus a natural part of their visit, and their `feature` is the focused feature they are about (if the focus names several, spread people over them). Everything else in the casting rules stays: varied people, their own words, a main reason that can lead on — never "test the calendar".
12
+
13
+ Split how they arrive, about half and half:
14
+ - **Straight in:** they land on a page of the focus itself (a bookmark, a shared link, a search result that opened it). This shows how the feature works for someone already there.
15
+ - **Find it:** they land on the front page or a related page — wherever their arrival really takes them — and the focus is something they would need to get to. This shows whether people can find it.
16
+
17
+ Do not add people who have nothing to do with the focus. In later rounds, cast new people for the same focus.
18
+
19
+ **Step 4 — unchanged.** The visitors are never told about the focus.
20
+
21
+ **Step 5 — open the report with the focus.** Before the usual report, using each visit's `Pages` line from `results` and the focus routes you confirmed:
22
+ - how many people reached the focus — of those who landed straight in, and of those who had to find it
23
+ - for those who had to find it and did not: where they went instead and where they stopped
24
+ - what happened inside the focus: how far people got, and what they said or did there
25
+
26
+ Then give the usual report for the rest of what visitors did.
@@ -5,6 +5,8 @@ How many people: {{count_line}}
5
5
 
6
6
  You are the coordinator. Virtual users will visit the site in a real browser, one sub-agent per person, and answer the Usercase survey cards they happen to meet. Their answers become findings in the Usercase dashboard.
7
7
 
8
+ {{focus}}
9
+
8
10
  ## 0. Setup (once)
9
11
 
10
12
  Call `setup` first. If it says it installed or updated the agent, ask the user to restart Claude Code and run this again — until then the visitors would use the old one, or none.
package/mcp/hub.js CHANGED
@@ -431,6 +431,7 @@ class Hub {
431
431
  // 개인정보 칸. 사이트가 막은 게 아니라 이 사람이 안 친 것이다 — 그렇게 알린다
432
432
  if (r.personalField !== undefined) said.push(`You don't type personal details such as your email, phone number or real name into this site on this visit, so you left that field empty. Carry on as you would without filling it in — skip it, or go elsewhere.`);
433
433
  if (r.closedOverlay) said.push("Something was open on top of the page; your click landed outside it and closed it, so it did not reach what you clicked.");
434
+ if (r.cappedTimes) said.push(`You did that ${r.cappedTimes} times in a row — that is the most in one go. What you see is after ${r.cappedTimes}; to go on, do it again.`);
434
435
  if (r.missed) said.push("Your click landed just outside it and nothing happened.");
435
436
  if (r.typos) said.push(`(You made ${r.typos} typo(s) and fixed them.)`);
436
437
  if (v.ended) {
@@ -750,6 +751,7 @@ function summarize(row) {
750
751
  const card = row.card ? `Card "${row.card.question}" → ${row.card.result === "close" ? "closed" : row.card.answer}` : "No card";
751
752
  return [
752
753
  `${row.person} (${row.device}) · ${row.entry?.how ?? "?"} → ${row.entry?.path ?? "/"} · ${row.steps.length} steps · ${Math.round(row.ms / 1000)}s`,
754
+ row.pages?.length ? `Pages: ${row.pages.join(" → ")}` : "",
753
755
  card,
754
756
  last ? `Last: ${last.action} — ${last.why}` : row.blocked ? "Stopped at a block page" : "",
755
757
  ]
package/mcp/server.js CHANGED
@@ -655,9 +655,13 @@ server.registerPrompt(
655
655
  "research",
656
656
  {
657
657
  description: "Research this site with Usercase virtual users",
658
- argsSchema: { url: z.string().optional(), count: z.string().optional().describe('How many people (default 5), or "unlimited" to keep going until you stop it') },
658
+ argsSchema: {
659
+ url: z.string().optional(),
660
+ count: z.string().optional().describe('How many people (default 5), or "unlimited" to keep going until you stop it'),
661
+ focus: z.string().optional().describe("Only people who come for this part of the site, e.g. calendar, or comments and sharing"),
662
+ },
659
663
  },
660
- ({ url, count }) => {
664
+ ({ url, count, focus }) => {
661
665
  const plan = researchPlan(count);
662
666
  run = { total: plan.total, since: Date.now() };
663
667
  // 사람 만드는 규칙은 우리 AI 로 만들 때와 같은 원본(people.md)에서 가져온다 — 두 벌로 두지 않는다.
@@ -669,8 +673,14 @@ server.registerPrompt(
669
673
  .replace(/\r\n/g, "\n")
670
674
  .replace(/^# Role:.*\n+/, "")
671
675
  .replace(/^#/gm, "###");
676
+ // focus 가 있으면 그 기능에 올 사람만 — 바뀌는 단계만 따로 적은 글을 앞에 붙인다. 방문자는 모른다
677
+ const f = (focus || "").trim();
678
+ const focusPart = f
679
+ ? fs.readFileSync(path.join(__dirname, "..", "agents", "focus.md"), "utf8").replace(/\r\n/g, "\n").replaceAll("{{focus}}", f).trim() + "\n\n"
680
+ : "";
672
681
  const body = fs
673
682
  .readFileSync(path.join(__dirname, "..", "agents", "research.md"), "utf8")
683
+ .replace("{{focus}}\n", focusPart)
674
684
  .replace("{{url}}", url || "(ask the user)")
675
685
  .replace("{{count_line}}", plan.line)
676
686
  .replace("{{rounds}}", plan.rounds)
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "@usercase/mcp",
3
- "version": "0.2.1",
4
- "description": "Virtual users that browse your website and tell you what they thought — an MCP server for Claude Code",
5
- "license": "UNLICENSED",
6
- "homepage": "https://usercase.io",
7
- "type": "commonjs",
8
- "main": "mcp/server.js",
9
- "bin": {
10
- "usercase-mcp": "mcp/server.js"
11
- },
12
- "files": [
13
- "mcp/",
14
- "src/browser/",
15
- "src/persona/",
16
- "src/run/",
17
- "src/prompts/",
18
- "agents/",
19
- "README.md"
20
- ],
21
- "engines": {
22
- "node": ">=18"
23
- },
24
- "publishConfig": {
25
- "access": "public"
26
- },
27
- "scripts": {
28
- "check": "node --check mcp/server.js && node scripts/onboarding.js --check && node scripts/agents.js --check",
29
- "docs": "node scripts/onboarding.js && node scripts/agents.js",
30
- "release:next": "node scripts/release.js next",
31
- "release:promote": "node scripts/release.js promote",
32
- "release:rollback": "node scripts/release.js rollback",
33
- "release:status": "node scripts/release.js status"
34
- },
35
- "dependencies": {
36
- "@modelcontextprotocol/sdk": "^1.30.0",
37
- "playwright": "^1.58.2",
38
- "zod": "^4.6.5"
39
- },
40
- "devDependencies": {
41
- "@google/genai": "^2.23.0"
42
- }
43
- }
1
+ {
2
+ "name": "@usercase/mcp",
3
+ "version": "0.2.2-next.0",
4
+ "description": "Virtual users that browse your website and tell you what they thought — an MCP server for Claude Code",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://usercase.io",
7
+ "type": "commonjs",
8
+ "main": "mcp/server.js",
9
+ "bin": {
10
+ "usercase-mcp": "mcp/server.js"
11
+ },
12
+ "files": [
13
+ "mcp/",
14
+ "src/browser/",
15
+ "src/persona/",
16
+ "src/run/",
17
+ "src/prompts/",
18
+ "agents/",
19
+ "README.md"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "check": "node --check mcp/server.js && node scripts/onboarding.js --check && node scripts/agents.js --check",
29
+ "docs": "node scripts/onboarding.js && node scripts/agents.js",
30
+ "release:next": "node scripts/release.js next",
31
+ "release:promote": "node scripts/release.js promote",
32
+ "release:rollback": "node scripts/release.js rollback",
33
+ "release:status": "node scripts/release.js status"
34
+ },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.30.0",
37
+ "playwright": "^1.58.2",
38
+ "zod": "^4.6.5"
39
+ },
40
+ "devDependencies": {
41
+ "@google/genai": "^2.23.0"
42
+ }
43
+ }
@@ -188,14 +188,16 @@ async function listControls(page, max = 60) {
188
188
  // 같은 크기로 여럿 되풀이되는 아이콘(달력 칸마다의 표시)부터 뺀다. 한두 개뿐인 것(이전·다음 화살표)은 남긴다
189
189
  const bySize = {};
190
190
  for (const c of inView) if (isIcon(c)) bySize[c.size] = (bySize[c.size] || 0) + 1;
191
- const keep = new Set(
192
- inView
193
- .filter(isIcon)
194
- .map((c, k) => ({ c, k, rep: bySize[c.size] }))
195
- .sort((a, b) => a.rep - b.rep || a.k - b.k)
196
- .slice(0, Math.max(0, max - named))
197
- .map((x) => x.c),
198
- );
191
+ const ranked = inView
192
+ .filter(isIcon)
193
+ .map((c, k) => ({ c, k, rep: bySize[c.size] }))
194
+ .sort((a, b) => a.rep - b.rep || a.k - b.k);
195
+ // 한두 개뿐인 아이콘(이전·다음 화살표, 닫기)은 이름 있는 것만으로 넘쳐도 남긴다 — 일정이 꽉 찬 달에서
196
+ // 이름 있는 칸만 100개가 넘어 화살표가 빠지고, 달을 못 넘겼다(10/20 실험실 p11, bluetags 2023년 9월)
197
+ const keep = new Set([
198
+ ...ranked.filter((x) => x.rep <= 2).slice(0, 4).map((x) => x.c),
199
+ ...ranked.slice(0, Math.max(0, max - named)).map((x) => x.c),
200
+ ]);
199
201
  pool = pool.filter((c) => c.where !== "in view" || !isIcon(c) || keep.has(c));
200
202
  iconsLeft = shown.length - pool.length;
201
203
  }
package/src/run/visit.js CHANGED
@@ -436,6 +436,8 @@ class Visit {
436
436
  const record = { step: this.steps.length + 1, t: Date.now() - this.t0, path: view.path, action: decision.action, why: decision.why, label: item?.label ?? null };
437
437
  const times = Math.max(1, Math.min(MAX_TIMES[decision.action] || 1, Math.round(Number(decision.times) || 1)));
438
438
  if (times > 1) record.times = times;
439
+ // 한 번에 할 수 있는 수보다 많이 달라고 했다. 말없이 줄이면 "사이트가 클릭을 삼킨다" 로 읽었다(10/20 실험실 p7: 15·20·30번이 다 4번)
440
+ if (Math.round(Number(decision.times) || 1) > times) record.cappedTimes = times;
439
441
  const before = page.url();
440
442
  const offsiteBefore = this.offsite.length;
441
443
  const notSentBefore = this.notSent.length;
@@ -615,7 +617,7 @@ class Visit {
615
617
 
616
618
  this.steps.push(record);
617
619
  this.history.push(
618
- `${decision.action}${times > 1 ? ` ×${times}` : ""}${item ? ` "${item.label}"` : ""} — ${decision.why}${record.skipped ? ` (${record.skipped})` : ""}${record.offsite ? ` (it leads to another website, ${record.offsite})` : ""}${record.notSent ? " (it would have sent something to the site; it was not sent)" : ""}${record.fileWindow ? " (a window to choose a file opened; you closed it without choosing)" : ""}${record.personalField !== undefined ? " (you chose not to enter personal details there)" : ""}${record.openedApp ? " (it tried to open another app; nothing opened)" : ""}`,
620
+ `${decision.action}${times > 1 ? ` ×${times}` : ""}${item ? ` "${item.label}"` : ""} — ${decision.why}${record.skipped ? ` (${record.skipped})` : ""}${record.cappedTimes ? " (that is the most in one go)" : ""}${record.offsite ? ` (it leads to another website, ${record.offsite})` : ""}${record.notSent ? " (it would have sent something to the site; it was not sent)" : ""}${record.fileWindow ? " (a window to choose a file opened; you closed it without choosing)" : ""}${record.personalField !== undefined ? " (you chose not to enter personal details there)" : ""}${record.openedApp ? " (it tried to open another app; nothing opened)" : ""}`,
619
621
  );
620
622
  this.log(`${record.step}. ${decision.action}${item ? ` "${String(item.label).slice(0, 30)}"` : ""}${record.skipped ? ` · ${record.skipped}` : ""} — ${decision.why}`);
621
623
  this.lastView = null;