@usercase/mcp 0.2.8-next.1 → 0.2.8-next.2
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/new-research.md +69 -0
- package/mcp/report.js +5 -0
- package/mcp/server.js +95 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,14 @@ Claude reads your question, asks you when it should appear, writes the steps, an
|
|
|
103
103
|
|
|
104
104
|
Claude suggests the moment, the question and its answers, and saves it only after you approve — as a draft in your Inbox. Nobody sees it until you turn it on there.
|
|
105
105
|
|
|
106
|
+
## Planning a research
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
/mcp__usercase__new-research
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
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 and their success conditions (the page that means done, or the exact button), and saves the research only after you approve it — as a draft in your Inbox, where you create it. Afterwards, `/mcp__usercase__path <research id>` records each task's expected path.
|
|
113
|
+
|
|
106
114
|
## What the visitors will and won't do
|
|
107
115
|
|
|
108
116
|
- 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,69 @@
|
|
|
1
|
+
# Plan a Usercase research
|
|
2
|
+
|
|
3
|
+
A Usercase research invites visitors of this site to do a few tasks while Usercase records how they go, then asks them questions. You plan one with the user: choose what is worth learning from the project's data, then write the tasks and questions. Unlike the dashboard's AI draft, **you may read the code**, so paths, buttons and success conditions come from the real site, not guesses.
|
|
4
|
+
|
|
5
|
+
## 1. Ask what they want to learn
|
|
6
|
+
|
|
7
|
+
Ask the user, in one short question, whether there is something they already want to learn, or whether you should look at the data and suggest something. Skip the question if they already said.
|
|
8
|
+
|
|
9
|
+
## 2. Read the data
|
|
10
|
+
|
|
11
|
+
Call `research_context`. Weigh it in this order:
|
|
12
|
+
1. **Findings from real visitors** — the strongest reason for a research. Findings marked "needs research" are the usual candidates; ones that "can be fixed directly" usually need a fix, not a research.
|
|
13
|
+
2. **Findings and leave words from virtual users** — useful, but they come from AI visitors. Whenever a suggestion rests on them, say so.
|
|
14
|
+
3. **Where people left** — a place many leave is worth a look, but by itself it does not say why.
|
|
15
|
+
|
|
16
|
+
Leave out what an existing research or a waiting draft already covers.
|
|
17
|
+
|
|
18
|
+
**When there is no data yet** (no visits and no findings), tell the user plainly and offer two ways:
|
|
19
|
+
- send a few virtual users first with `{{research_cmd}}` (limited to the part of the site they care about, if they named one), then run this command again with their visits as evidence;
|
|
20
|
+
- or go ahead without data: from what the user wants to learn, or from the code alone. Anything you suggest from the code alone is a guess to test, and you say it that way.
|
|
21
|
+
|
|
22
|
+
Do what they choose. If they already know what they want to learn, you do not need data to go on.
|
|
23
|
+
|
|
24
|
+
## 3. Read the code
|
|
25
|
+
|
|
26
|
+
For the parts of the site involved, read the routes and the components: the pages a participant passes through, the controls they would use, the visible text on those controls, what has to be true for the goal to be reached (signed in, an item in a list). If real visits exist, call `question_context` too: the paths and words visitors actually saw. Where the code and the visits disagree, trust the visits.
|
|
27
|
+
|
|
28
|
+
What you learn from the code helps you write the success conditions. It never goes into what participants read.
|
|
29
|
+
|
|
30
|
+
## 4. Suggest, unless they already chose
|
|
31
|
+
|
|
32
|
+
If the user did not name a topic, suggest two or three, each in a few lines:
|
|
33
|
+
- what the team would learn, and the decision it would help with,
|
|
34
|
+
- the evidence, labelled by source (real visitors / virtual users / code only),
|
|
35
|
+
- the part of the site a participant would walk through.
|
|
36
|
+
|
|
37
|
+
Let the user pick one or change it.
|
|
38
|
+
|
|
39
|
+
## 5. Write the research
|
|
40
|
+
|
|
41
|
+
One research is **one flow a participant can walk in one sitting**: 1–4 tasks, about 5–10 minutes in all.
|
|
42
|
+
|
|
43
|
+
**Tasks**
|
|
44
|
+
- The title is a goal in the participant's words: what they want to get done, not how. Never name a button, a menu, a page or a route — that hands them the answer.
|
|
45
|
+
- Use `info_text` only when the participant needs a situation to act in.
|
|
46
|
+
- Choose the success condition from the code:
|
|
47
|
+
- `url_entry` — reaching a page proves the goal: `target_url` is that page's path, with `:name` for the parts that change (names from the route definitions).
|
|
48
|
+
- `element_click` — the goal is done at a click: `xpath_url` is the page where the click happens, `click_text` is the element's exact visible text, `xpath` finds the element by that text.
|
|
49
|
+
- `time_on_page` — the goal is to read or understand something: `timer` is the seconds that mean they read it.
|
|
50
|
+
- Only goals the site really supports, and reachable from where participants start. Do not make anyone pay, delete or send things that affect other people.
|
|
51
|
+
|
|
52
|
+
**Questions**
|
|
53
|
+
- At most 2 follow-up questions per task, about that task as they just experienced it. At most 5 questions at the end, about the whole experience and the evidence that led here.
|
|
54
|
+
- Neutral wording that does not suggest the answer. One thing per question.
|
|
55
|
+
- Choice answers do not overlap and cover the likely cases; add a free-text question when you cannot guess the answers. `likert_scale` always gets labels for both ends.
|
|
56
|
+
|
|
57
|
+
**Invite**: the page glob where visitors should be invited (where the flow begins, or the moment the evidence points to), and one short line asking them to take part.
|
|
58
|
+
|
|
59
|
+
**Language**: English, unless the user asks for another language.
|
|
60
|
+
|
|
61
|
+
## 6. Show the user, then save
|
|
62
|
+
|
|
63
|
+
Show the whole research compactly: the purpose and its evidence, each task with how it counts as done (in plain words: "done when they reach the order page"), the questions, and the invite. Change whatever they ask for.
|
|
64
|
+
|
|
65
|
+
On their approval, call `research_save` with `basis` (`real` if the evidence came from real visitors, `virtual` if only from virtual users, `none` otherwise) and the ids of the findings it answers. Give them the Inbox link it returns and tell them:
|
|
66
|
+
- it stays a draft until they create it in the Inbox, where they also set when the invite appears;
|
|
67
|
+
- after it is created, `{{path_cmd}}` with the research id records each task's expected path, so the results can tell who went straight from who took a detour.
|
|
68
|
+
|
|
69
|
+
Do not change any code.
|
package/mcp/report.js
CHANGED
|
@@ -287,6 +287,9 @@ const saveQuestion = (body) => keyed("POST", "/agent/questions", body, 60000);
|
|
|
287
287
|
/** New question 창(Several steps) — 창에 쓴 것을 읽고, 조건만 돌려보낸다 */
|
|
288
288
|
const getStepsBrief = (session) => keyed("GET", `/agent/questions/steps/${encodeURIComponent(session)}`);
|
|
289
289
|
const sendSteps = (session, body) => keyed("PUT", `/agent/questions/steps/${encodeURIComponent(session)}`, body, 60000);
|
|
290
|
+
/** 리서치 초안(new-research) — 무엇을 리서치할지 고를 근거와, Inbox 초안으로 저장 */
|
|
291
|
+
const getResearchContext = () => keyed("GET", "/agent/researches/context", null, 30000);
|
|
292
|
+
const saveResearchDraft = (body) => keyed("POST", "/agent/researches/drafts", body, 30000);
|
|
290
293
|
/** 대시보드 주소. API 주소에서 /koa 를 뗀다 */
|
|
291
294
|
const dashboardUrl = (p) => apiBase().replace(/\/koa$/, "") + p;
|
|
292
295
|
|
|
@@ -307,5 +310,7 @@ module.exports = {
|
|
|
307
310
|
saveQuestion,
|
|
308
311
|
getStepsBrief,
|
|
309
312
|
sendSteps,
|
|
313
|
+
getResearchContext,
|
|
314
|
+
saveResearchDraft,
|
|
310
315
|
dashboardUrl,
|
|
311
316
|
};
|
package/mcp/server.js
CHANGED
|
@@ -634,6 +634,94 @@ server.registerTool(
|
|
|
634
634
|
wrap(async ({ success }) => text(await recorder.save({ success }))),
|
|
635
635
|
);
|
|
636
636
|
|
|
637
|
+
// ── 리서치 초안 (메인 에이전트, new-research 절차) ──
|
|
638
|
+
// 대시보드 New research 의 AI 초안은 문장 한 줄과 사이트 주소만 본다. 여기서는 데이터로 고르고, 코드를 읽어 경로·버튼을 채운다.
|
|
639
|
+
// 저장은 Inbox 리서치 초안으로 — 리서치로 만드는 건 사람이 Inbox 에서 한다
|
|
640
|
+
|
|
641
|
+
const counts = (rows, key) => (rows.length ? rows.map((r) => `${r[key]} (${r.n})`).join(" · ") : "none");
|
|
642
|
+
|
|
643
|
+
server.registerTool(
|
|
644
|
+
"research_context",
|
|
645
|
+
{
|
|
646
|
+
description:
|
|
647
|
+
"What this project's data says is worth researching: open findings, where real visitors and virtual users left, what virtual users said when they left without getting what they came for, and the researches and drafts that already exist. Call first in the new-research procedure.",
|
|
648
|
+
inputSchema: {},
|
|
649
|
+
},
|
|
650
|
+
wrap(async () => {
|
|
651
|
+
const c = await report.getResearchContext();
|
|
652
|
+
const L = [`Project: "${c.project.title}"${c.project.website ? ` — ${c.project.website}` : ""}`, `Last ${c.days} days.`, ""];
|
|
653
|
+
L.push(`## Real visitors: ${c.real.visits} visits`);
|
|
654
|
+
if (c.real.visits) L.push(`Where they left: ${counts(c.real.exits, "left_at")}`);
|
|
655
|
+
L.push("", `## Virtual users: ${c.virtual.visits} visits, ${c.virtual.leftUnhappy} left without getting what they came for`);
|
|
656
|
+
if (c.virtual.visits) {
|
|
657
|
+
L.push(`Why they left: ${counts(c.virtual.reasons, "left_reason")}`, `Where they left: ${counts(c.virtual.exits, "left_at")}`);
|
|
658
|
+
if (c.virtual.unhappyWords.length) L.push("Their words when leaving unhappy, most recent first:", ...c.virtual.unhappyWords.map((w) => `- ${w.at}: ${w.why}`));
|
|
659
|
+
}
|
|
660
|
+
L.push("", `## Open findings not yet researched: ${c.findings.length}`);
|
|
661
|
+
for (const f of c.findings)
|
|
662
|
+
L.push(
|
|
663
|
+
`- [${f.id}] (${f.source === "real" ? "real visitors" : "virtual users"}, ${f.answers} answers, ${f.nextStep === "research" ? "needs research" : "can be fixed directly"}) ${f.title}`,
|
|
664
|
+
` on ${f.page || "any page"} · asked: "${f.question}"`,
|
|
665
|
+
` seen: ${f.observation}`,
|
|
666
|
+
` likely: ${f.hypothesis}`,
|
|
667
|
+
);
|
|
668
|
+
L.push("", "## Already there");
|
|
669
|
+
L.push(`Researches: ${c.researches.length ? c.researches.map((r) => `"${r.title}" (${r.status})`).join(" · ") : "none"}`);
|
|
670
|
+
L.push(`Drafts waiting in the Inbox: ${c.drafts.length ? c.drafts.map((t) => `"${t}"`).join(" · ") : "none"}`);
|
|
671
|
+
if (!c.real.visits && !c.virtual.visits && !c.findings.length) L.push("", "■ There is no data yet — no visits and no findings.");
|
|
672
|
+
return text(L.join("\n"));
|
|
673
|
+
}),
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const RQuestion = z.object({
|
|
677
|
+
title: z.string().describe("The question, as the participant reads it"),
|
|
678
|
+
subtitle: z.string().optional(),
|
|
679
|
+
type: z.enum(["short_answer", "long_answer", "likert_scale", "yes_no", "single_choice", "multiple_choice"]),
|
|
680
|
+
choices: z.array(z.string()).optional().describe("single_choice / multiple_choice: 2–8 answers"),
|
|
681
|
+
scale_labels: z.array(z.string()).length(2).optional().describe("likert_scale: what 1 and what 5 mean"),
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
server.registerTool(
|
|
685
|
+
"research_save",
|
|
686
|
+
{
|
|
687
|
+
description:
|
|
688
|
+
"Save the research as a DRAFT in the dashboard Inbox, after the user approved it. It becomes a research only when someone creates it there. Paths are page paths starting with / (no host), with :name for the parts that change.",
|
|
689
|
+
inputSchema: {
|
|
690
|
+
title: z.string().describe("Short, for the team"),
|
|
691
|
+
purpose: z.string().describe("What the team wants to learn and why — the evidence it came from"),
|
|
692
|
+
start_screen: z.object({ title: z.string(), subtitle: z.string() }).optional().describe("What participants read before the first task"),
|
|
693
|
+
tasks: z
|
|
694
|
+
.array(
|
|
695
|
+
z.object({
|
|
696
|
+
title: z.string().describe("The goal, in the participant's words. Never names buttons, menus or pages"),
|
|
697
|
+
info_text: z.string().optional().describe("The situation the participant is in, if needed"),
|
|
698
|
+
success_condition: z.enum(["url_entry", "element_click", "time_on_page"]),
|
|
699
|
+
target_url: z.string().optional().describe("url_entry: the page path that means done"),
|
|
700
|
+
xpath_url: z.string().optional().describe("element_click: the page path where the click happens"),
|
|
701
|
+
click_text: z.string().optional().describe("element_click: the exact visible text of the element"),
|
|
702
|
+
xpath: z.string().optional().describe("element_click: XPath that finds that element by its text"),
|
|
703
|
+
timer: z.number().int().optional().describe("time_on_page: seconds on the page that mean done"),
|
|
704
|
+
follow_up_questions: z.array(RQuestion).optional().describe("Asked right after this task, at most 2"),
|
|
705
|
+
}),
|
|
706
|
+
)
|
|
707
|
+
.optional(),
|
|
708
|
+
questions: z.array(RQuestion).optional().describe("Asked after all tasks"),
|
|
709
|
+
invite: z.object({ page: z.string().describe("Page glob where visitors are invited"), copy: z.string().describe("The invite line, short") }).optional(),
|
|
710
|
+
basis: z.enum(["real", "virtual", "none"]).describe("What the research came from: real visitor data, virtual user data, or neither (the user's question or the code)"),
|
|
711
|
+
findingIds: z.array(z.string()).optional().describe("Ids of the findings it answers, from research_context"),
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
wrap(async (args) => {
|
|
715
|
+
const r = await report.saveResearchDraft(args);
|
|
716
|
+
return text(
|
|
717
|
+
[
|
|
718
|
+
`Saved as a draft${r.findings ? `, linked to ${r.findings} finding${r.findings > 1 ? "s" : ""}` : ""}.`,
|
|
719
|
+
`Open it in the Inbox to review and create it: ${report.dashboardUrl(r.inboxPath)}`,
|
|
720
|
+
].join("\n"),
|
|
721
|
+
);
|
|
722
|
+
}),
|
|
723
|
+
);
|
|
724
|
+
|
|
637
725
|
// ── 가상 사용자 서브에이전트용 ──
|
|
638
726
|
|
|
639
727
|
// Codex 는 도구마다 처음 부를 때 허용을 묻는다. 방문 도중에 물으면 그동안 첫 화면에 가만히 있던 사람이 된다(10/28 첫 행동 130초).
|
|
@@ -751,7 +839,7 @@ const doc = (name) => fs.readFileSync(path.join(__dirname, "..", "agents", name)
|
|
|
751
839
|
/** 조건 쓰는 규칙은 question·steps 가 같이 쓴다 — 한 벌만 둔다 */
|
|
752
840
|
const agentDoc = (name, vars = {}) => {
|
|
753
841
|
let body = doc(name).replace("{{condition_rules}}", doc("condition.md").trim());
|
|
754
|
-
for (const [k, v] of Object.entries(vars)) body = body.
|
|
842
|
+
for (const [k, v] of Object.entries(vars)) body = body.replaceAll(`{{${k}}}`, v);
|
|
755
843
|
return body;
|
|
756
844
|
};
|
|
757
845
|
|
|
@@ -819,6 +907,11 @@ const COMMANDS = {
|
|
|
819
907
|
description: "Make a Usercase question for a precise moment on your site — your agent suggests the moment, question and answers",
|
|
820
908
|
body: () => agentDoc("question.md"),
|
|
821
909
|
},
|
|
910
|
+
// 리서치 초안 — 무엇을 알고 싶은지는 대화로 묻는다(인자 없음, question 과 같은 이유)
|
|
911
|
+
"new-research": {
|
|
912
|
+
description: "Plan a Usercase research from your data and code — your agent suggests what to research, writes the tasks and questions, and saves a draft",
|
|
913
|
+
body: () => agentDoc("new-research.md", { research_cmd: cmd("research"), path_cmd: cmd("path") }),
|
|
914
|
+
},
|
|
822
915
|
// 프로젝트 바꾸기 — 연결은 저장소 폴더마다라 이 저장소만 바뀐다
|
|
823
916
|
connect: {
|
|
824
917
|
description: "Connect this repository to a Usercase project, or switch it to another one",
|
|
@@ -845,7 +938,7 @@ if (PLUGIN || CODEX || OTHER) {
|
|
|
845
938
|
"instructions",
|
|
846
939
|
{
|
|
847
940
|
description:
|
|
848
|
-
"The step-by-step instructions for a Usercase command (research, install, path, question, steps, connect). The plugin's /usercase:* commands call this; follow what it returns as the user's request.",
|
|
941
|
+
"The step-by-step instructions for a Usercase command (research, install, path, question, new-research, steps, connect). The plugin's /usercase:* commands call this; follow what it returns as the user's request.",
|
|
849
942
|
inputSchema: {
|
|
850
943
|
command: z.enum(Object.keys(COMMANDS)),
|
|
851
944
|
...COMMANDS.research.argsSchema,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usercase/mcp",
|
|
3
|
-
"version": "0.2.8-next.
|
|
3
|
+
"version": "0.2.8-next.2",
|
|
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",
|