@usercase/mcp 0.2.8-next.3 → 0.2.8-next.4
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 +1 -1
- package/agents/new-research.md +20 -9
- package/mcp/path.js +36 -7
- package/mcp/server.js +22 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ Claude suggests the moment, the question and its answers, and saves it only afte
|
|
|
109
109
|
/mcp__usercase__new-research
|
|
110
110
|
```
|
|
111
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
|
|
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, 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.
|
|
113
113
|
|
|
114
114
|
## What the visitors will and won't do
|
|
115
115
|
|
package/agents/new-research.md
CHANGED
|
@@ -63,24 +63,35 @@ One research is **one flow a participant can walk in one sitting**: 1–4 tasks,
|
|
|
63
63
|
|
|
64
64
|
**Language**: English, unless the user asks for another language.
|
|
65
65
|
|
|
66
|
-
## 6.
|
|
66
|
+
## 6. Plan each task's route
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
The results tell who went straight from who took a detour only when a task has an expected path: the route the site was built for. Work it out now, from the code, for every task with a page or click to reach (tasks done by staying on a page have none):
|
|
69
|
+
- **where it starts**: the page a participant is on when the task begins;
|
|
70
|
+
- **the clicks a participant must make**, in order — the main route the UI offers, not a shortcut only you know;
|
|
71
|
+
- **where it ends**: the page or the click that counts as done. It must agree with the task's success condition.
|
|
72
|
+
- what the route needs: a signed-in demo account, or a write the safety guard does not send (the last click is then recorded without pressing it).
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
## 7. Show the user everything at once
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
Show the whole research compactly: the purpose and its evidence, each task with how it counts as done (in plain words, no XPath) and its route in one line, the questions, and the invite. Say that on their approval you will walk each route in a browser and save the draft with the routes. Change whatever they ask for.
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
## 8. On approval: record the routes, then save
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
Do not ask again between these steps. For each task with a route, in order, use the task id `plan-<n>` (n = the task's position in the research, from 1):
|
|
77
81
|
|
|
78
82
|
{{path_walk}}
|
|
79
83
|
|
|
80
|
-
###
|
|
84
|
+
### Save each route
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
When `path_check` passes and the route ends where the plan said, call `path_save` (`page` or `click` when the route's end is the task's success condition, otherwise `keep`). It is kept until the draft is saved. Stop and ask the user only when a route cannot be walked as planned — the site differs from the code, a check keeps failing, or the route needs a write or a sign-in you do not have; leave that task without a route if they say so.
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
### Save the draft
|
|
89
|
+
|
|
90
|
+
Then 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. It uploads the routes you saved with the draft. Tell the user:
|
|
91
|
+
- the Inbox link, and that it stays a draft until they create it there, where they also set when the invite appears;
|
|
92
|
+
- which tasks have a route and the screenshots `path_check` listed for them, and any task left without one and why;
|
|
93
|
+
- a draft that is discarded, or not created within 30 days, loses its routes.
|
|
94
|
+
|
|
95
|
+
To redo a route later: `path_start` with `draft:<draft>:<index>` while the draft exists, or `{{path_cmd}}` with the research id once it is created.
|
|
85
96
|
|
|
86
97
|
Do not change any code.
|
package/mcp/path.js
CHANGED
|
@@ -124,9 +124,24 @@ class PathRecorder {
|
|
|
124
124
|
this.hub = hub;
|
|
125
125
|
/** taskId → { research, task } — path_tasks 로 받아 둔다 */
|
|
126
126
|
this.known = new Map();
|
|
127
|
+
/** 아직 저장 안 한 리서치의 과제 경로(plan-<번호>) — 번호 → { body, steps, how }. research_save 가 가져간다 */
|
|
128
|
+
this.planned = new Map();
|
|
127
129
|
this.cur = null;
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
/** 녹화해 둔 계획 과제 경로를 넘기고 비운다 */
|
|
133
|
+
takePlanned() {
|
|
134
|
+
const out = new Map(this.planned);
|
|
135
|
+
this.planned.clear();
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 상대 주소를 풀 사이트 — 연결된 프로젝트의 주소. 못 읽으면 빈 값(전체 주소를 받는다) */
|
|
140
|
+
async projectSite() {
|
|
141
|
+
if (this.site === undefined) this.site = await report.getInstall().then((m) => m.website || "").catch(() => "");
|
|
142
|
+
return this.site;
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
/** 리서치의 과제들 */
|
|
131
146
|
async tasks(researchId) {
|
|
132
147
|
const r = await report.getResearchTasks(researchId);
|
|
@@ -169,20 +184,24 @@ class PathRecorder {
|
|
|
169
184
|
|
|
170
185
|
/** 새로 기록을 시작한다. 같은 과제를 다시 하면 처음부터 */
|
|
171
186
|
async start({ task, url, device = "desktop", account = null }) {
|
|
172
|
-
|
|
187
|
+
// 아직 저장 안 한 리서치의 과제(new-research, plan-<번호>) — 녹화해 두었다가 research_save 가 초안과 함께 올린다
|
|
188
|
+
const plan = String(task).match(/^plan-(\d+)$/i);
|
|
189
|
+
const k = plan ? { research: { website: await this.projectSite() }, task: { title: `Planned task ${plan[1]}` } } : this.known.get(String(task));
|
|
173
190
|
if (!k)
|
|
174
191
|
throw new Error(
|
|
175
192
|
String(task).startsWith("draft:")
|
|
176
193
|
? `Draft task ${task} is not known in this session. Draft tasks are known right after research_save; once the research is created, use path_tasks with its id instead.`
|
|
177
194
|
: `Task ${task} is not known yet. Call path_tasks with the research id first.`,
|
|
178
195
|
);
|
|
179
|
-
|
|
196
|
+
if (!/^https?:\/\//i.test(url) && !k.research.website) throw new Error("Give the full URL where the route starts — the project has no site address to resolve a path against.");
|
|
197
|
+
const full = /^https?:\/\//i.test(url) ? url : new URL(url, k.research.website).toString();
|
|
180
198
|
await this.stop();
|
|
181
199
|
const s = this.hub.site(full);
|
|
182
200
|
const ctx = await this.open(s, { device, account });
|
|
183
201
|
const page = await ctx.newPage();
|
|
184
202
|
ws.ensure();
|
|
185
|
-
|
|
203
|
+
// 폴더 이름에 : 를 못 쓰는 OS 가 있다(Windows) — draft:<초안>:<번호> 같은 id 를 바꿔 쓴다
|
|
204
|
+
const dir = path.join(ws.DIR, "paths", String(task).replace(/[^\w.-]+/g, "_"));
|
|
186
205
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
187
206
|
fs.mkdirSync(dir, { recursive: true });
|
|
188
207
|
this.cur = { taskId: String(task), title: k.task.title, s, ctx, page, net: watchNet(page), dir, startUrl: full, device, account, steps: [], actions: [], view: null, checked: null };
|
|
@@ -419,15 +438,25 @@ class PathRecorder {
|
|
|
419
438
|
};
|
|
420
439
|
if (success === "page") body.success = { condition: "url_entry", targetUrl: last.url };
|
|
421
440
|
else if (success === "click") body.success = { condition: "element_click", xpath: last.xpath, xpathUrl: last.url, classes: last.classes, ids: last.ids, label: last.label || "" };
|
|
422
|
-
|
|
441
|
+
const cutLine = cut.length
|
|
442
|
+
? `Left out the ${cut.length} step(s) after the success point (${cut.map((s) => (s.type === "URL" ? `page ${s.url}` : `click "${s.label || s.tag}"`)).join(", ")}) — the task ends at the success point, so no participant could ever reach them. Tell the user.`
|
|
443
|
+
: "";
|
|
444
|
+
// 아직 저장 안 한 리서치의 과제 — 올리지 않고 들고 있다가 research_save 가 초안과 함께 올린다
|
|
445
|
+
const plan = c.taskId.match(/^plan-(\d+)$/i);
|
|
446
|
+
if (plan) {
|
|
447
|
+
const n = Number(plan[1]);
|
|
448
|
+
const how = success === "page" ? `done on reaching ${last.url}` : success === "click" ? `done on clicking "${last.label || last.tag}" on ${last.url}` : "success condition as written";
|
|
449
|
+
this.planned.set(n, { body, steps: steps.length, how });
|
|
450
|
+
await this.stop();
|
|
451
|
+
return [`Kept the checked path for planned task ${n}: ${steps.length} steps, ${how}. research_save uploads it with the draft.`, cutLine].filter(Boolean).join("\n");
|
|
452
|
+
}
|
|
453
|
+
// 초안 과제(draft:<초안>:<번호>)는 초안에 둔다 — 리서치로 만들 때 옮겨진다
|
|
423
454
|
const draft = c.taskId.match(/^draft:([^:]+):(\d+)$/);
|
|
424
455
|
const r = draft ? await report.saveDraftPath(draft[1], Number(draft[2]), body) : await report.savePath(c.taskId, body);
|
|
425
456
|
const where = draft ? "The path is kept in the draft and moves into the research when it is created." : "The dashboard's task results now separate going straight from detours.";
|
|
426
457
|
const done = [
|
|
427
458
|
`Saved the expected path for task ${c.taskId} ("${c.title}"): ${r.steps} steps${r.success ? `, success condition set to ${r.success === "url_entry" ? `reaching ${last.url}` : `clicking "${last.label || last.tag}"`}` : ""}. ${where}`,
|
|
428
|
-
|
|
429
|
-
? `Left out the ${cut.length} step(s) after the success point (${cut.map((s) => (s.type === "URL" ? `page ${s.url}` : `click "${s.label || s.tag}"`)).join(", ")}) — the task ends at the success point, so no participant could ever reach them. Tell the user.`
|
|
430
|
-
: "",
|
|
459
|
+
cutLine,
|
|
431
460
|
]
|
|
432
461
|
.filter(Boolean)
|
|
433
462
|
.join("\n");
|
package/mcp/server.js
CHANGED
|
@@ -566,7 +566,7 @@ server.registerTool(
|
|
|
566
566
|
description:
|
|
567
567
|
"Open a fresh browser on the page where the task's intended route begins and start recording its expected path. The page itself is step 1. Starting again discards the recording in progress.",
|
|
568
568
|
inputSchema: {
|
|
569
|
-
task: z.string().describe("Task id from path_tasks"),
|
|
569
|
+
task: z.string().describe("Task id from path_tasks · plan-<n> for task n of a research not saved yet (new-research) · draft:<draft>:<index> for a saved draft"),
|
|
570
570
|
url: z.string().describe("Full URL where the route starts — the local dev server is fine; only paths are saved"),
|
|
571
571
|
device: z.enum(["desktop", "mobile"]).optional(),
|
|
572
572
|
account: z.string().optional().describe("Demo account saved by login (a1 …) if the route needs a signed-in user"),
|
|
@@ -685,7 +685,7 @@ server.registerTool(
|
|
|
685
685
|
"research_save",
|
|
686
686
|
{
|
|
687
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.",
|
|
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. Expected paths recorded and saved beforehand as plan-<n> (n = the task's position in tasks, from 1) are uploaded with it.",
|
|
689
689
|
inputSchema: {
|
|
690
690
|
title: z.string().describe("Short, for the team"),
|
|
691
691
|
purpose: z.string().describe("What the team wants to learn and why — the evidence it came from"),
|
|
@@ -713,19 +713,32 @@ server.registerTool(
|
|
|
713
713
|
},
|
|
714
714
|
wrap(async (args) => {
|
|
715
715
|
const r = await report.saveResearchDraft(args);
|
|
716
|
-
// 과제 경로 녹화(path_start)에 쓸 번호 — 옛 서버는 tasks 를 안 준다
|
|
717
716
|
const tasks = r.tasks || [];
|
|
718
717
|
if (tasks.length) recorder.addDraft(r.draftId, { title: args.title, website: r.website || "", tasks });
|
|
718
|
+
// 저장 전에 녹화해 둔 경로(plan-<번호>, 번호는 tasks 순서 1부터)를 초안의 그 과제에 올린다
|
|
719
|
+
const planned = recorder.takePlanned();
|
|
720
|
+
const lines = [];
|
|
721
|
+
for (const [n, p] of [...planned.entries()].sort((a, b) => a[0] - b[0])) {
|
|
722
|
+
const t = tasks[n - 1];
|
|
723
|
+
if (!t) {
|
|
724
|
+
lines.push(`- planned task ${n}: not uploaded — the research has ${tasks.length} task(s)`);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
await report.saveDraftPath(r.draftId, t.index, p.body);
|
|
729
|
+
lines.push(`- task ${n} "${t.title}": ${p.steps} steps, ${p.how}`);
|
|
730
|
+
} catch (e) {
|
|
731
|
+
lines.push(`- task ${n} "${t.title}": not uploaded (${e.message}) — record it again with path_start draft:${r.draftId}:${t.index}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const missing = tasks.filter((t, i) => !planned.has(i + 1) && t.condition !== "time_on_page");
|
|
719
735
|
return text(
|
|
720
736
|
[
|
|
721
737
|
`Saved as a draft${r.findings ? `, linked to ${r.findings} finding${r.findings > 1 ? "s" : ""}` : ""}.`,
|
|
722
738
|
`Open it in the Inbox to review and create it: ${report.dashboardUrl(r.inboxPath)}`,
|
|
723
|
-
...(
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
`Task ids for recording their expected paths with path_start${r.website ? ` (relative URLs resolve against ${r.website})` : ""}:`,
|
|
727
|
-
...tasks.map((t) => `- draft:${r.draftId}:${t.index} — ${t.title}`),
|
|
728
|
-
]
|
|
739
|
+
...(lines.length ? ["", "Expected paths saved with it:", ...lines] : []),
|
|
740
|
+
...(missing.length
|
|
741
|
+
? ["", "Tasks without an expected path:", ...missing.map((t) => `- ${t.title} (record later with path_start draft:${r.draftId}:${t.index})`)]
|
|
729
742
|
: []),
|
|
730
743
|
].join("\n"),
|
|
731
744
|
);
|
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.4",
|
|
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",
|