@usercase/mcp 0.2.8-next.2 → 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 +33 -5
- package/mcp/path.js +53 -10
- package/mcp/report.js +3 -0
- package/mcp/server.js +40 -3
- 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
|
@@ -56,14 +56,42 @@ One research is **one flow a participant can walk in one sitting**: 1–4 tasks,
|
|
|
56
56
|
|
|
57
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
58
|
|
|
59
|
+
**Wording people read** (the start screen, the invite line):
|
|
60
|
+
- Say plainly what they will do and roughly how long it takes.
|
|
61
|
+
- The participant is the subject. Never ask for help, never make the team or the site the subject, never thank in advance.
|
|
62
|
+
- Do not mention the problem you suspect or what others said.
|
|
63
|
+
|
|
59
64
|
**Language**: English, unless the user asks for another language.
|
|
60
65
|
|
|
61
|
-
## 6.
|
|
66
|
+
## 6. Plan each task's route
|
|
67
|
+
|
|
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).
|
|
73
|
+
|
|
74
|
+
## 7. Show the user everything at once
|
|
75
|
+
|
|
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.
|
|
77
|
+
|
|
78
|
+
## 8. On approval: record the routes, then save
|
|
79
|
+
|
|
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):
|
|
81
|
+
|
|
82
|
+
{{path_walk}}
|
|
83
|
+
|
|
84
|
+
### Save each route
|
|
85
|
+
|
|
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.
|
|
87
|
+
|
|
88
|
+
### Save the draft
|
|
62
89
|
|
|
63
|
-
|
|
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.
|
|
64
94
|
|
|
65
|
-
|
|
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.
|
|
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.
|
|
68
96
|
|
|
69
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);
|
|
@@ -150,6 +165,12 @@ class PathRecorder {
|
|
|
150
165
|
.join("\n");
|
|
151
166
|
}
|
|
152
167
|
|
|
168
|
+
/** 방금 저장한 리서치 초안의 과제들(new-research) — draft:<초안>:<번호> 로 path_start 에 쓴다 */
|
|
169
|
+
addDraft(draftId, { title, website, tasks }) {
|
|
170
|
+
const research = { title, status: "draft", website };
|
|
171
|
+
for (const t of tasks) this.known.set(`draft:${draftId}:${t.index}`, { research, task: { taskId: `draft:${draftId}:${t.index}`, title: t.title } });
|
|
172
|
+
}
|
|
173
|
+
|
|
153
174
|
async open(s, { device, account }) {
|
|
154
175
|
let storageState;
|
|
155
176
|
if (account) {
|
|
@@ -163,15 +184,24 @@ class PathRecorder {
|
|
|
163
184
|
|
|
164
185
|
/** 새로 기록을 시작한다. 같은 과제를 다시 하면 처음부터 */
|
|
165
186
|
async start({ task, url, device = "desktop", account = null }) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
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));
|
|
190
|
+
if (!k)
|
|
191
|
+
throw new Error(
|
|
192
|
+
String(task).startsWith("draft:")
|
|
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.`
|
|
194
|
+
: `Task ${task} is not known yet. Call path_tasks with the research id first.`,
|
|
195
|
+
);
|
|
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();
|
|
169
198
|
await this.stop();
|
|
170
199
|
const s = this.hub.site(full);
|
|
171
200
|
const ctx = await this.open(s, { device, account });
|
|
172
201
|
const page = await ctx.newPage();
|
|
173
202
|
ws.ensure();
|
|
174
|
-
|
|
203
|
+
// 폴더 이름에 : 를 못 쓰는 OS 가 있다(Windows) — draft:<초안>:<번호> 같은 id 를 바꿔 쓴다
|
|
204
|
+
const dir = path.join(ws.DIR, "paths", String(task).replace(/[^\w.-]+/g, "_"));
|
|
175
205
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
176
206
|
fs.mkdirSync(dir, { recursive: true });
|
|
177
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 };
|
|
@@ -407,13 +437,26 @@ class PathRecorder {
|
|
|
407
437
|
steps: steps.map((s) => ({ type: s.type, url: s.url, xpath: s.xpath || "", classes: s.classes || [], ids: s.ids || [], fingerprint: s.fingerprint || null, image: fs.readFileSync(s.image).toString("base64") })),
|
|
408
438
|
};
|
|
409
439
|
if (success === "page") body.success = { condition: "url_entry", targetUrl: last.url };
|
|
410
|
-
else if (success === "click") body.success = { condition: "element_click", xpath: last.xpath, xpathUrl: last.url, classes: last.classes, ids: last.ids };
|
|
411
|
-
const
|
|
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 || "" };
|
|
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:<초안>:<번호>)는 초안에 둔다 — 리서치로 만들 때 옮겨진다
|
|
454
|
+
const draft = c.taskId.match(/^draft:([^:]+):(\d+)$/);
|
|
455
|
+
const r = draft ? await report.saveDraftPath(draft[1], Number(draft[2]), body) : await report.savePath(c.taskId, body);
|
|
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.";
|
|
412
457
|
const done = [
|
|
413
|
-
`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}"`}` : ""}.
|
|
414
|
-
|
|
415
|
-
? `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.`
|
|
416
|
-
: "",
|
|
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}`,
|
|
459
|
+
cutLine,
|
|
417
460
|
]
|
|
418
461
|
.filter(Boolean)
|
|
419
462
|
.join("\n");
|
package/mcp/report.js
CHANGED
|
@@ -279,6 +279,8 @@ const getResearchTasks = (researchId) => keyed("GET", `/agent/researches/${encod
|
|
|
279
279
|
|
|
280
280
|
/** 과제의 정상 경로를 통째로 바꾼다. 화면을 올리느라 오래 걸릴 수 있다 */
|
|
281
281
|
const savePath = (taskId, body) => keyed("PUT", `/agent/tasks/${encodeURIComponent(taskId)}/path`, body, 90000);
|
|
282
|
+
/** 아직 리서치가 안 된 초안 과제의 경로(new-research) — 초안에 둔다 */
|
|
283
|
+
const saveDraftPath = (draftId, index, body) => keyed("PUT", `/agent/researches/drafts/${encodeURIComponent(draftId)}/tasks/${index}/path`, body, 90000);
|
|
282
284
|
|
|
283
285
|
/** 질문 만들기(question) — 최근 방문을 읽어야 해서 조금 걸린다 */
|
|
284
286
|
const getQuestionContext = () => keyed("GET", "/agent/questions/context", null, 60000);
|
|
@@ -305,6 +307,7 @@ module.exports = {
|
|
|
305
307
|
sendDiagnostics,
|
|
306
308
|
getResearchTasks,
|
|
307
309
|
savePath,
|
|
310
|
+
saveDraftPath,
|
|
308
311
|
getQuestionContext,
|
|
309
312
|
checkQuestion,
|
|
310
313
|
saveQuestion,
|
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,10 +713,33 @@ server.registerTool(
|
|
|
713
713
|
},
|
|
714
714
|
wrap(async (args) => {
|
|
715
715
|
const r = await report.saveResearchDraft(args);
|
|
716
|
+
const tasks = r.tasks || [];
|
|
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");
|
|
716
735
|
return text(
|
|
717
736
|
[
|
|
718
737
|
`Saved as a draft${r.findings ? `, linked to ${r.findings} finding${r.findings > 1 ? "s" : ""}` : ""}.`,
|
|
719
738
|
`Open it in the Inbox to review and create it: ${report.dashboardUrl(r.inboxPath)}`,
|
|
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})`)]
|
|
742
|
+
: []),
|
|
720
743
|
].join("\n"),
|
|
721
744
|
);
|
|
722
745
|
}),
|
|
@@ -843,6 +866,19 @@ const agentDoc = (name, vars = {}) => {
|
|
|
843
866
|
return body;
|
|
844
867
|
};
|
|
845
868
|
|
|
869
|
+
/** path.md 의 "## 3. Walk it" ~ "## 4. Check it" 끝까지 — 제목은 한 단계 낮추고 번호를 뗀다 */
|
|
870
|
+
const pathWalkRules = () => {
|
|
871
|
+
const d = doc("path.md");
|
|
872
|
+
const from = d.indexOf("## 3. Walk it");
|
|
873
|
+
const to = d.indexOf("## 5.");
|
|
874
|
+
if (from < 0 || to < 0) throw new Error("agents/path.md lost its Walk it / Check it sections");
|
|
875
|
+
return d
|
|
876
|
+
.slice(from, to)
|
|
877
|
+
.replace(/^## \d+\. /gm, "### ")
|
|
878
|
+
.replace(/\n*Do not change any code\.\n*/g, "\n\n")
|
|
879
|
+
.trim();
|
|
880
|
+
};
|
|
881
|
+
|
|
846
882
|
/**
|
|
847
883
|
* 명령 지시문. 따로 쓰면 MCP 프롬프트(/mcp__usercase__research), 플러그인이면 instructions 도구(/usercase:research 스킬이 부른다)로
|
|
848
884
|
* 같은 글이 나간다
|
|
@@ -910,7 +946,8 @@ const COMMANDS = {
|
|
|
910
946
|
// 리서치 초안 — 무엇을 알고 싶은지는 대화로 묻는다(인자 없음, question 과 같은 이유)
|
|
911
947
|
"new-research": {
|
|
912
948
|
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
|
-
|
|
949
|
+
// 경로를 걷고 확인하는 규칙은 path 명령과 한 벌 — path.md 의 3·4 단계를 그대로 싣는다
|
|
950
|
+
body: () => agentDoc("new-research.md", { research_cmd: cmd("research"), path_cmd: cmd("path"), path_walk: pathWalkRules() }),
|
|
914
951
|
},
|
|
915
952
|
// 프로젝트 바꾸기 — 연결은 저장소 폴더마다라 이 저장소만 바뀐다
|
|
916
953
|
connect: {
|
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",
|