@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 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 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. It then offers to walk each task in a browser and record its expected path; the paths stay with the draft and 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.
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
 
@@ -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. Show the user, then save
66
+ ## 6. Plan each task's route
67
67
 
68
- Show the whole research compactly: the purpose and its evidence, each task with how it counts as done (in plain words, no paths or XPath), the questions, and the invite. Change whatever they ask for.
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
- 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 it stays a draft until they create it in the Inbox, where they also set when the invite appears.
74
+ ## 7. Show the user everything at once
71
75
 
72
- ## 7. Record each task's expected path
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
- The results tell who went straight from who took a detour only when a task has an expected path, and walking it in a browser confirms the success condition you wrote from the code. Ask the user whether to record the paths now. If they say no, tell them `{{path_cmd}}` with the research id does it after the research is created, and stop.
78
+ ## 8. On approval: record the routes, then save
75
79
 
76
- For each task with a page or click to reach (skip tasks that are done by staying on a page), use the task id `research_save` listed for it, `draft:<draft>:<number>`:
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
- ### Show the user, then save
84
+ ### Save each route
81
85
 
82
- Show the user the steps and the screenshots `path_check` lists. Ask whether this is the route they intend, and whether the task should be done on reaching the last page (`page`), on the last recorded click (`click`), or keep the condition you wrote (`keep`). With page or click the path ends at that point — say so when the route goes on after it.
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
- On their approval, call `path_save`. The path stays in the draft and moves into the research when it is created; a draft that is discarded, or not created within 30 days, loses its paths. When all tasks are done, tell the user which have a path and any you could not record and why.
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
- const k = this.known.get(String(task));
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
- const full = /^https?:\/\//i.test(url) ? url : new URL(url, k.research.website || undefined).toString();
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
- const dir = path.join(ws.DIR, "paths", String(task));
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
- // 초안 과제(new-research, draft:<초안>:<번호>)는 초안에 둔다 — 리서치로 만들 때 옮겨진다
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
- cut.length
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
- ...(tasks.length
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",
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",