@usercase/mcp 0.2.10-next.3 → 0.2.10-next.5
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 +8 -0
- package/agents/research.md +2 -1
- package/agents/study.md +2 -0
- package/mcp/hub.js +26 -2
- package/mcp/server.js +26 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,14 @@ The number is how many people visit in total (default 5). Above 5, Claude works
|
|
|
67
67
|
|
|
68
68
|
With `unlimited` it keeps going until you press Esc. Every visit uses your Claude usage.
|
|
69
69
|
|
|
70
|
+
### Take part in a research
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
/mcp__usercase__research https://staging.example.com 10 research=<research id>
|
|
76
|
+
```
|
|
77
|
+
|
|
70
78
|
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.
|
|
71
79
|
|
|
72
80
|
### Sites behind a sign-in
|
package/agents/research.md
CHANGED
|
@@ -41,6 +41,7 @@ If important parts of the site are only reachable when signed in (a dashboard, "
|
|
|
41
41
|
- **the sign-in cookie**: whether it has a lifetime (`maxAge`, `ttl`, `expires`) or lasts only until the browser closes
|
|
42
42
|
- **how long a sign-in lasts**, in hours
|
|
43
43
|
- anything that would stop a demo account: required email verification, invite-only sign-up
|
|
44
|
+
- **whether one account can be signed in in several browsers at the same time**: whether a new sign-in or a refresh replaces the earlier session (one active session per account, rotating refresh tokens, a session bound to the device or IP). Visitors on the same account share one saved session in separate browsers.
|
|
44
45
|
|
|
45
46
|
Then decide:
|
|
46
47
|
- A lasting cookie (most sites): open `login` with the default window. Google and other buttons work there.
|
|
@@ -93,7 +94,7 @@ For each person, start a `{{visitor}}` sub-agent{{spawn}} with a prompt like:
|
|
|
93
94
|
|
|
94
95
|
> Call open_visit with person "<id>" and live this visit as that person until you leave.
|
|
95
96
|
|
|
96
|
-
Start the whole round at once (at most 5 run at the same time). Do not give them anything else — no hints about the site, no instructions about what to find or which card to answer.
|
|
97
|
+
Start the whole round at once (at most 5 run at the same time) — except members: if the code shows one account cannot be used in several browsers at once, or you could not tell, never run two members on the same account at the same time: give each member an `account` in `save_people` so you know which one they use, start the next member on an account when the visit on it has ended, and run newcomers alongside. If it can be used at once, members run together like everyone else. Do not give them anything else — no hints about the site, no instructions about what to find or which card to answer.
|
|
97
98
|
|
|
98
99
|
If any visitor reports the site is blocking us, stop starting new ones and tell the user.
|
|
99
100
|
|
package/agents/study.md
CHANGED
|
@@ -10,6 +10,8 @@ Show the user the tasks in one short list before casting, so they know what the
|
|
|
10
10
|
|
|
11
11
|
**The research decides what the visit is about — do not ask the user about it.** Not what the visitors should try, how far they should go, which tasks to cover or what to focus on: the participants get the research's tasks from the study panel and do what they would do. The only questions for the user are the ones the steps below always ask — demo accounts (if parts of the tasks need sign-in) and whether writes are sent — asked as those questions, in those words, and not merged with anything about the research. A task that needs a sign-in the visitors do not have is still run: what happens to a participant without an account is a result.
|
|
12
12
|
|
|
13
|
+
**Tasks that need a sign-in.** Read the tasks against the code: if a task can only be done signed in (following, saving, a "my" page, posting), the demo-account question is required in this run, not optional — say which task needs it when you ask. If the user has accounts, cast both members and newcomers, so the research shows what members do and what newcomers run into. If they have none, say once that those tasks will show only what newcomers run into, and go on.
|
|
14
|
+
|
|
13
15
|
**Step 3 — cast participants for this research.** Each person is someone who really comes to this site and who would accept this research's invitation — people its purpose is about. Their `goal` is still their own reason for being on the site, in their own words; **do not write the tasks into the goal or anywhere else**. The visitors read the tasks themselves in the study panel, as real participants do — a visitor told the task by you knows more than a participant would, and the research loses its value.
|
|
14
16
|
- Vary them the way real participants vary: devices, how familiar they are with the site, patience, how carefully they read. Some will finish every task, some will get stuck — that is what the research is for.
|
|
15
17
|
- Give each person a `landing` page where the research's invitation would meet them: the page the first task starts from, if the tasks or the code make it clear; otherwise where they would arrive on their own.
|
package/mcp/hub.js
CHANGED
|
@@ -29,6 +29,8 @@ const report = require("./report.js");
|
|
|
29
29
|
|
|
30
30
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
31
|
/** 리서치가 아직 진행 중인 위젯 상태(core StateManager WidgetState). 이 밖이면 과제가 끝난 것이다 */
|
|
32
|
+
/** 패널을 기다리는 가장 긴 시간 — 과제의 실패 시간은 대개 5분(대시보드 기본) */
|
|
33
|
+
const STUDY_WAIT_MS = 5 * 60 * 1000;
|
|
32
34
|
const STUDY_ACTIVE = new Set(["consent", "loading", "testing", "timeout", "survey", "next", "submit"]);
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -427,7 +429,8 @@ class Hub {
|
|
|
427
429
|
external,
|
|
428
430
|
study: research
|
|
429
431
|
? {
|
|
430
|
-
|
|
432
|
+
// wait — 과제를 그만 두고 패널을 기다린다. 위젯 단추가 아니라 도구가 기다린다(그동안 방문자는 아무것도 안 한다)
|
|
433
|
+
press: (k) => (k === "wait" ? this.studyWait(rec) : study.press(page, k)),
|
|
431
434
|
// 과제 질문·시간 초과 창이 페이지를 덮고 있나 — 그 사이 조작은 페이지에 닿지 않는다
|
|
432
435
|
blocking: async () => {
|
|
433
436
|
const w = study.isSurveyUrl(page.url()) ? null : await study.readStudy(page);
|
|
@@ -459,7 +462,8 @@ class Hub {
|
|
|
459
462
|
"",
|
|
460
463
|
"A small invitation popped up asking you to take part in a short study on this site, and you agreed: you accepted it and allowed your screen to be recorded.",
|
|
461
464
|
"The study panel in the corner of the screen now shows your first task. Do the tasks as yourself, on the site, in your own way — the panel knows when a task is done and moves on by itself.",
|
|
462
|
-
"
|
|
465
|
+
"You agreed to take part, so you stay with the study until its tasks are done — you do not leave the site in the middle of it over a small thing. The study may ask you a few questions along the way; answer them with answer_card.",
|
|
466
|
+
"If you get stuck on a task, keep trying in your own way for as long as you would. When you would give up on that task, wait on the study panel (it is in the list): studies often offer a way to give up on a task after a while, and then you go on to the next task. Leave in the middle only if you really would — the study never offers a way on, or something about you or your moment makes you stop.",
|
|
463
467
|
);
|
|
464
468
|
if (!sdk.ready) lines.push("(note for whoever runs this: the Usercase SDK did not start on this page — this visit may not be recorded)");
|
|
465
469
|
lines.push("", await this.screen(rec));
|
|
@@ -477,6 +481,8 @@ class Hub {
|
|
|
477
481
|
if (look.card) return renderCard(look.card);
|
|
478
482
|
// 과제 건너뛰기는 위젯 단추다. 화면 목록 끝에 붙인다 — 누르면 Visit 이 위젯을 누른다
|
|
479
483
|
if (st && st.skip && !look.view.dialog) look.view.items.push({ i: look.view.items.length, label: `${st.skip} (in the study panel — gives up on this task)`, where: "study panel, corner of the screen", study: "skip" });
|
|
484
|
+
// 아직 건너뛸 수 없는 과제 — 그만 두고 패널을 기다릴 수 있다. 실패 시간이 있는 과제면 그 사이 패널이 포기를 받아 준다
|
|
485
|
+
else if (st && st.panel && !look.view.dialog) look.view.items.push({ i: look.view.items.length, label: "Wait on the study panel (stop trying this task for now)", where: "study panel, corner of the screen", study: "wait" });
|
|
480
486
|
// 브라우저 창(confirm 등)이 떠 있다. 페이지는 멈춰 있다 — 아이콘 그림·위치는 안 본다
|
|
481
487
|
if (look.view.dialog) {
|
|
482
488
|
this.pictures.set(rec.id, null);
|
|
@@ -557,6 +563,10 @@ class Hub {
|
|
|
557
563
|
}
|
|
558
564
|
st.lastTaskKey = key;
|
|
559
565
|
st.lastTask = s.task.title;
|
|
566
|
+
if (st.waitNote) {
|
|
567
|
+
note = note ? `${note} ${st.waitNote}` : st.waitNote;
|
|
568
|
+
st.waitNote = null;
|
|
569
|
+
}
|
|
560
570
|
return { panel: renderStudyPanel(s.task), skip: s.task.skip, note };
|
|
561
571
|
}
|
|
562
572
|
// 과제가 다 끝났다. 끝난 뒤 설문이 있으면 SDK 가 새 탭으로 열고, 그 탭은 이 탭으로 옮겨 온다(open 의 새 탭 처리)
|
|
@@ -574,6 +584,20 @@ class Hub {
|
|
|
574
584
|
return null;
|
|
575
585
|
}
|
|
576
586
|
|
|
587
|
+
/**
|
|
588
|
+
* 과제를 그만 두고 패널을 기다린다 — 실패 시간이 있는 과제면 SDK 가 "어렵나요?" 창(timeout)을 띄우고 건너뛰기를 열어 준다.
|
|
589
|
+
* 실패 시간이 없는 과제는 아무것도 안 온다. 다 기다리고도 그대로면 방문자에게 그렇게 알린다(그다음은 방문자가 정한다).
|
|
590
|
+
* 막힌 참여자가 패널이 열어 주기 전에 사이트를 떠나, 30명 중 27명이 과제 2에서 기록 없이 사라졌다(10/30 라이브 bluetags)
|
|
591
|
+
*/
|
|
592
|
+
async studyWait(rec) {
|
|
593
|
+
const st = rec.study;
|
|
594
|
+
const s = await study.waitState(rec.page, ["timeout", "survey", "next", "submit", "done", "complete", "idle"], STUDY_WAIT_MS);
|
|
595
|
+
if (!s || s.state === "testing") {
|
|
596
|
+
st.waited = (st.waited || 0) + 1;
|
|
597
|
+
st.waitNote = "(You waited about five minutes on the study panel. It has not offered a way to give up on this task yet.)";
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
577
601
|
/** 과제 질문·시간 초과 창에 답한다. 참여하기로 한 사람이라 닫지 않는다 — 비워 두고 넘기는 건 된다(필수가 아니면) */
|
|
578
602
|
async studyAnswer(rec, decision) {
|
|
579
603
|
const p = rec.studyPending;
|
package/mcp/server.js
CHANGED
|
@@ -928,6 +928,30 @@ const pathWalkRules = () => {
|
|
|
928
928
|
.trim();
|
|
929
929
|
};
|
|
930
930
|
|
|
931
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* 명령 인자를 이름으로 맞춘다. Claude Code 는 /mcp__usercase__research 의 인자를 띄어쓰기로 나눠 순서대로 넘긴다 —
|
|
935
|
+
* `research=<id>` 라고 써도 세 번째 자리(focus)에 "research=<id>" 로 들어가, 리서치가 아니라 초점 판으로 돌았다(10/30 라이브 bluetags).
|
|
936
|
+
* 그래서 어느 자리에 있든 `이름=값`(또는 `이름:값`)이면 그 이름으로 옮기고, research 를 받는 명령에서는 이름 없는 리서치 id(UUID)도 research 로 본다
|
|
937
|
+
*/
|
|
938
|
+
function byName(args = {}, schema = {}) {
|
|
939
|
+
const names = Object.keys(schema || {});
|
|
940
|
+
const out = {};
|
|
941
|
+
const loose = [];
|
|
942
|
+
for (const [k, raw] of Object.entries(args || {})) {
|
|
943
|
+
if (raw == null) continue;
|
|
944
|
+
const v = String(raw).trim();
|
|
945
|
+
const m = v.match(/^([a-z]+)\s*[=:]\s*(.*)$/is);
|
|
946
|
+
if (m && names.includes(m[1].toLowerCase())) out[m[1].toLowerCase()] = m[2].trim();
|
|
947
|
+
else if (names.includes("research") && UUID.test(v.replace(/^research\s+/i, ""))) out.research = v.replace(/^research\s+/i, "");
|
|
948
|
+
else loose.push([k, v]);
|
|
949
|
+
}
|
|
950
|
+
// 이름 없이 온 값은 제자리에 — 이름으로 이미 채운 자리는 덮지 않는다
|
|
951
|
+
for (const [k, v] of loose) if (!(k in out) && v !== "") out[k] = v;
|
|
952
|
+
return out;
|
|
953
|
+
}
|
|
954
|
+
|
|
931
955
|
/**
|
|
932
956
|
* 명령 지시문. 따로 쓰면 MCP 프롬프트(/mcp__usercase__research), 플러그인이면 instructions 도구(/usercase:research 스킬이 부른다)로
|
|
933
957
|
* 같은 글이 나간다
|
|
@@ -1043,12 +1067,12 @@ if (PLUGIN || CODEX || OTHER) {
|
|
|
1043
1067
|
...COMMANDS.steps.argsSchema,
|
|
1044
1068
|
},
|
|
1045
1069
|
},
|
|
1046
|
-
wrap(async ({ command, ...args }) => text(COMMANDS[command].body(args))),
|
|
1070
|
+
wrap(async ({ command, ...args }) => text(COMMANDS[command].body(byName(args, COMMANDS[command].argsSchema)))),
|
|
1047
1071
|
);
|
|
1048
1072
|
} else {
|
|
1049
1073
|
for (const [name, c] of Object.entries(COMMANDS)) {
|
|
1050
1074
|
const meta = c.argsSchema ? { description: c.description, argsSchema: c.argsSchema } : { description: c.description };
|
|
1051
|
-
server.registerPrompt(name, meta, (args = {}) => ({ messages: [{ role: "user", content: { type: "text", text: c.body(args) } }] }));
|
|
1075
|
+
server.registerPrompt(name, meta, (args = {}) => ({ messages: [{ role: "user", content: { type: "text", text: c.body(byName(args, c.argsSchema)) } }] }));
|
|
1052
1076
|
}
|
|
1053
1077
|
}
|
|
1054
1078
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usercase/mcp",
|
|
3
|
-
"version": "0.2.10-next.
|
|
3
|
+
"version": "0.2.10-next.5",
|
|
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",
|