@usercase/mcp 0.2.1 → 0.2.2-next.1
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/agents/focus.md +29 -0
- package/agents/research.md +2 -0
- package/mcp/hub.js +2 -0
- package/mcp/server.js +12 -2
- package/package.json +43 -43
- package/src/browser/page.js +10 -8
- package/src/run/visit.js +3 -1
package/agents/focus.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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, and ask.** After you build the feature map, find which features, pages and routes "{{focus}}" covers, and what people do there (e.g. for a chat: read it, join the room, send a message). Then **ask the user, as its own question and before the sign-in and writes questions**: what you understood the focus to cover (e.g. "the calendar = /calendar and the event detail popup"), and which of those actions they want people to really try. Wait for the answer. Asking about sign-in or writes does not count as this question. If you cannot find it on the map, or it could mean several things, say so in the same question — 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
|
+
- If it needs something done on the account first (joining a room, following, saving, an order), you do not know whether the demo accounts have done it. Ask the user in the sign-in question, or cast people who do it for the first time on this visit.
|
|
11
|
+
|
|
12
|
+
**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".
|
|
13
|
+
|
|
14
|
+
Split how they arrive, about half and half:
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
If the focus is only reachable when signed in, everyone is a member — this replaces "keep some as newcomers". Add newcomers only if the user asks for them (to see the sign-in wall). And cast the members to fit what the account has really done: if the user did not say a demo account already joined the room or saved the item, do not write people who are "already in the room" or "checking their saved list" — write people who join, save or start there on this visit.
|
|
19
|
+
|
|
20
|
+
Do not add people who have nothing to do with the focus. In later rounds, cast new people for the same focus.
|
|
21
|
+
|
|
22
|
+
**Step 4 — unchanged.** The visitors are never told about the focus.
|
|
23
|
+
|
|
24
|
+
**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:
|
|
25
|
+
- how many people reached the focus — of those who landed straight in, and of those who had to find it
|
|
26
|
+
- for those who had to find it and did not: where they went instead and where they stopped
|
|
27
|
+
- what happened inside the focus: how far people got, and what they said or did there
|
|
28
|
+
|
|
29
|
+
Then give the usual report for the rest of what visitors did.
|
package/agents/research.md
CHANGED
|
@@ -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: {
|
|
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.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
|
+
}
|
package/src/browser/page.js
CHANGED
|
@@ -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
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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;
|